The data race Rust refuses to compile, and the one line that fixes it
A data race is one of the nastiest bugs in software: two threads access the same memory at the same time and at least one is writing, so the result depends on timing and is different every run. In C, C++, Go, or Java, the code that causes it compiles cleanly and passes your tests, then corrupts data in production under load, where you cannot reproduce it. Rust made a promise that sounds too strong to be true: in safe Rust, a data race is a compile error. Not a warning, not a runtime panic, a program that does not build. As Rust cements its place in new systems code through 2026, this is the guarantee people mean when they say "fearless concurrency". Write the race and watch it happen.
The one idea
Rust tracks, at compile time, who owns each piece of data and who is allowed to touch it. The rule is simple and absolute: you can have many readers or one writer, never both at once. A thread is a piece of code that may run for an unknown amount of time, so if you hand data to a thread, Rust has to know that no one else can mutate it meanwhile. When you try to give the same mutable data to several threads, you are asking to violate the one-writer rule, and the compiler simply will not let the ownership work out. The data race is impossible to express in safe code, so it is impossible to ship.
Write the race
Three threads, each trying to modify the same vector:
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
let mut handles = vec![];
for i in 0..3 {
handles.push(thread::spawn(move || {
data[i] += 100; // each thread mutates the shared vector
}));
}
for h in handles { h.join().unwrap(); }
println!("{:?}", data);
}
In most languages this compiles and races. Rust stops at the build:
error[E0382]: borrow of moved value: `data`
6 | handles.push(thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
11 | println!("{:?}", data);
| ^^^^ value borrowed here after move
Read what it caught. To give the vector to the first thread, the closure has to take ownership of it, move. But then the second thread cannot also take it, it was already moved, and the third cannot either. There is exactly one data, and ownership can go to exactly one place. The compiler is not being fussy; it is telling you that "three threads share one mutable vector" is a contradiction, and it noticed before you ran it.
Fix it in the one honest way
The fix is to make the sharing explicit and safe: wrap the data in an Arc (an atomically reference-counted handle so several threads can co-own it) around a Mutex (a lock so only one thread touches the inside at a time). Now the type system has proof that access is serialized.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // shared ownership + a lock
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter); // each thread gets its own handle
handles.push(thread::spawn(move || {
let mut n = c.lock().unwrap(); // exclusive access while held
*n += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("final counter: {}", *counter.lock().unwrap());
}
final counter: 10
Three details that matter:
Arc::clonedoes not copy the data, it makes another handle to the same data and bumps a thread-safe reference count. Every thread co-owns the counter, and the memory is freed only when the last handle is gone. That solves "who owns it": everyone, safely.Mutexsolves "one writer at a time". You cannot read the number without callinglock(), and while you hold the lock no other thread can. The one-writer rule is now enforced at runtime by the lock, and the type system knows it, so it lets the code compile.- Rust could tell the first version was unsafe and the second was safe using two marker traits,
SendandSync, that every type carries. A plainMutex-wrapped value isSend; a bare&mutshared across threads is not. The whole guarantee reduces to the compiler checking these markers, no runtime race detector required.
Where this shows up
This is why Rust is being pulled into browsers, operating system kernels, and the infrastructure that government memory-safety guidance now pushes hard. Concurrency bugs are among the most expensive and least reproducible in all of software, and Rust converts a whole class of them from "found in production, maybe" to "found at compile time, always". The same ownership rules that stop the data race also stop use-after-free and iterator invalidation, one model catching a family of bugs.
If you want to build up from ownership to channels, atomics, and lock-free structures, all checked by the same compiler, that is the path the rust track on IWTLP takes one guarantee at a time.
Sources
- Fearless concurrency and the
Send/Syncmodel: The Rust Programming Language, Fearless Concurrency - Ownership and the borrow rules that make it work: The Rust Programming Language, Understanding Ownership