Rust Prevents Data Races, Not Race Conditions | corrode Rust Consulting
Rust Insights
Rust Prevents Data Races, Not Race Conditions
by Matthias Endler
Published: 2026-06-12
Safe Rust eliminates all data races. What it does not do is prevent race conditions in the broader sense: deadlocks, livelocks, and logic bugs in your synchronization.
What’s the difference?
These two terms get used interchangeably all the time, even by experienced developers, so it’s worth writing down exactly what Rust promises and what it does not.
What Is a Data Race?
To quote the Rustonomicon:
Safe Rust guarantees an absence of data races, which are defined as:
two or more threads concurrently accessing a location of memory
one or more of them is a write
one or more of them is unsynchronized
All three conditions have to hold at once. If every access is a read, there’s no data race. If the accesses are synchronized (say, behind a lock), there’s no data race. A data race is specifically unsynchronized concurrent access where at least one side writes.
This matters because a data race is Undefined Behavior!<br>A data race does not mean you might read a “stale” value.<br>It means the compiler is allowed to do anything like tear a write in half and reorder it.
And you can’t wave this away as a harmless race that happens to work out. As Raph Levien notes in With undefined behavior, anything is possible:
It used to be thought that data races could be classified into “benign” and dangerous categories, but research strongly suggests that the former category doesn’t exist.
In other words, every data race is a real bug!<br>And because it’s Undefined Behavior, the symptom can show up far away from the cause and much later,<br>in the form of a corrupted value, a crash, or a security hole that only appears under heavy load.
For example, here are two threads incrementing the same counter:
use std::thread;
fn main() {<br>let mut counter = 0;
thread::scope(|s| {<br>for _ in 0..2 {<br>s.spawn(|| {<br>counter += 1; // unsynchronized write to shared memory<br>});<br>});<br>In many languages, the equivalent compiles and runs, and two threads writing to counter at the same time can corrupt it. The result depends on timing, so the bug may not show up until the code runs under load.
In Rust, it doesn’t compile at all:
error[E0499]: cannot borrow `counter` as mutable more than once at a time<br>--> ex1_data_race.rs:8:21<br>8 | s.spawn(|| {<br>| - ^^ `counter` was mutably borrowed here<br>| in the previous iteration of the loop<br>9 | counter += 1;<br>| ------- borrows occur due to use of `counter` in closure<br>The borrow checker stops you before the program can exist. Two threads both want a mutable reference to counter, and Rust’s core rule is that you can never have two mutable references to the same data at the same time. The data race is impossible because the aliasing it requires is impossible.
This is the point the Nomicon makes:
Data races are prevented mostly through Rust’s ownership system alone: it’s impossible to alias a mutable reference, so it’s impossible to perform a data race.
Key takeaways
A data race is a specific thing: concurrent access, at least one write, no synchronization. All three at once.
A data race is Undefined Behavior, not just a wrong answer.
In purely safe Rust, data races are impossible , because they require aliasing a mutable reference, which the borrow checker forbids.
How Rust Lets You Share State Safely
So how do you increment a counter from two threads correctly? You make the access synchronized, which removes the third condition from the data race definition. Wrap the value in a Mutex, which lets only one thread touch it at a time:
use std::sync::Mutex;<br>use std::thread;
fn main() {<br>let counter = Mutex::new(0);
thread::scope(|s| {<br>for _ in 0..2 {<br>s.spawn(|| {<br>*counter.lock().unwrap() += 1;<br>});<br>});
println!("{}", counter.into_inner().unwrap());<br>This compiles, and it always prints 2.
The compiler enforces this through two marker traits, Send and Sync. Roughly: Send means a value can be moved to another thread, and Sync means it can be shared between threads by reference.
A plain i32 can’t be mutated through a shared reference, and a mutable reference can’t be copied across threads. To share and mutate it, you need a type that provides interior mutability while remaining thread-safe (Sync), which is exactly what Mutex does.
Try to share something that isn’t Sync, like an Rc or a RefCell, and you get a compile error.
(Here the threads can’t outlive counter, so they borrow it directly. If they needed to outlive the scope, say with thread::spawn, you’d wrap it in an Arc to share ownership: Arc> is the workhorse for that.)
That’s the whole idea. Rust pushes many concurrency-safety checks from runtime into the type system.
Key takeaways
Synchronized access is not a data race, so it’s allowed.
A Mutex is the standard way to share mutable state across threads (an Arc> when threads outlive their spawning scope).
The Send and Sync...