Short version for the impatient: Rust’s memory safety isn’t magic, it isn’t a runtime, and it isn’t free. It’s a compile-time bookkeeping system that refuses to let your program use memory it shouldn’t. If you already write C, C++, Go, or even TypeScript, this post is the on-ramp I wish I’d had two years ago.
I spent my first month with Rust genuinely angry at the borrow checker. By month three I was quietly annoyed when I went back to other languages and the compiler didn’t catch the same bugs. This is the tour I wish someone had handed me.
The actual bugs Rust prevents at compile time
Before the theory, here are the real-world bugs Rust refuses to let you ship:
- Use-after-free. You free a pointer and then read from it. C will happily return garbage. Rust won’t compile.
- Double free. You free the same pointer twice. C crashes in the allocator. Rust won’t compile.
- Null pointer dereference. You read from a pointer that’s
NULL. Rust has no null pointers at all; theOptiontype forces you to handle the “nothing here” case explicitly. - Data race. Two threads write to the same piece of memory without synchronization. Rust’s type system refuses to send certain values between threads unless they’re wrapped in a synchronization primitive.
- Iterator invalidation. You modify a vector while iterating over it. C++ lets you. Rust won’t compile.
- Buffer overflows on slices. Indexing past the end of a slice panics at runtime rather than silently reading adjacent memory.
Microsoft and Google have both published data showing memory-safety issues account for ~70% of the critical vulnerabilities in their C/C++ codebases. See Microsoft’s numbers in A proactive approach to more secure code and Google’s parallel finding in Memory safe languages in Android 13. Rust’s whole value proposition is eliminating that 70% without paying for a garbage collector.
Ownership in one paragraph, with code
Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped (freed). You can pass ownership around, but only one place holds it at a time. That’s it. That’s the entire model.
Here’s the C version of a bug that ships in real codebases:
// C: use after free, compiles fine, crashes in prod
char* greeting() {
char buf[32];
strcpy(buf, "hello");
return buf; // returning a pointer to a stack buffer that
// is about to be reclaimed. classic.
}
The same logical mistake in Rust:
// Rust: compile error. the borrow checker refuses.
fn greeting() -> &str {
let buf = String::from("hello");
&buf // error[E0515]: cannot return reference to local variable
}
buf owns the string data. The function scope owns buf. When the function returns, buf gets dropped, and any reference to it would dangle. The compiler traces the lifetime and refuses. You either return ownership (-> String) or accept a reference from the caller (fn greeting(buf: &mut String)).
Once you internalize that every value has one owner and references cannot outlive the owner, most of the borrow checker errors start making sense. The ownership chapter of the Rust Book is 40 minutes of reading and it’s the best 40 minutes you’ll spend learning the language.
Borrowing: the rule everyone trips over
You can have either:
- Any number of immutable references (
&T), or - Exactly one mutable reference (
&mut T).
Never both at the same scope. This is the rule that felt punitive the first week and felt essential by the fourth.
Here’s the data-race pattern Rust rejects at compile time:
fn main() {
let mut items = vec![1, 2, 3];
let first = &items[0];
items.push(4); // error: cannot borrow `items` as mutable
println!("{}", first); // because `first` still borrows it
}
first is an immutable reference into items. If items.push(4) reallocates the vector, first would become a dangling pointer. In C++ this is the “iterator invalidation” bug that ate weekends for decades. Rust sees the two overlapping borrows and says no.
Fix it by ending the first borrow before starting the second:
fn main() {
let mut items = vec![1, 2, 3];
let first_value = items[0]; // copy the number out, borrow ends
items.push(4); // fine now
println!("{}", first_value);
}
I don’t know how many C++ bugs I wrote before Rust made this pattern mechanically impossible. The borrow checker isn’t keeping you from writing fast code. It’s refusing to let you write unsound code.
Lifetimes are just scope math
'a in a function signature looks intimidating. It isn’t. A lifetime annotation tells the compiler “this reference is valid as long as this other reference is valid.”
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
This signature says: the returned reference lives as long as the shorter of a and b. The compiler can’t figure that out by itself when two inputs and an output are involved. You tell it, and it holds you to it.
Most of the time you don’t even write lifetimes. Rust has elision rules that infer them for common cases. By the end of your first week the 'a noise will feel like no big deal.
What about unsafe? Isn’t this all a lie?
Yes and no. Rust has an unsafe keyword that lets you dereference raw pointers, call FFI functions, and do things the borrow checker can’t prove are sound. The point of unsafe is not “safety off.” It’s “I’ve audited this block myself and I’m telling the compiler to trust me.”
99% of application code never writes unsafe. Library authors use it sparingly to wrap a safe API around something the compiler can’t verify (atomic operations, custom allocators, SIMD). The Rustonomicon is the book for that 1%, and if you’re writing application code you can happily ignore it for your first year.
Where Rust’s memory safety earns its keep
I use Rust when the memory safety story pays for the extra compile-time friction:
- Long-running servers. A web service that can’t leak, can’t crash on a null pointer, and can’t race itself into corrupt state. I cover some of the tradeoffs in my Rust web frameworks comparison.
- Data pipelines. Parsing huge inputs where one wild pointer means “corrupt output silently for two weeks before anyone notices.”
- FFI layers. Wrapping a C library for a higher-level language. Rust is excellent at being the safe membrane.
- Anything touching untrusted input. Parsers, decoders, network servers. The historical hit rate for memory safety CVEs in this category is ugly.
I don’t reach for Rust to write a quick script, or a throwaway prototype, or anything where iteration speed matters more than correctness. That’s fine. Rust isn’t trying to be every language.
For a broader take on when I pick Rust vs the alternatives you can read my piece on Rust vs Go in 2026. And more of the systems work I do is on my portfolio’s about page if you want context.
The mental model that unblocked me
Here’s what finally made it click. Stop thinking of the borrow checker as a syntax policeman. Start thinking of it as a design reviewer that asks “who owns this data, and who is allowed to read or write it right now?”
Every borrow checker error is that reviewer asking one of three questions:
- “Is this pointer still pointing at a live owner?”
- “Is anyone else looking at this while you’re mutating it?”
- “Does this reference outlive the thing it’s borrowing from?”
When I started reading compiler errors as questions rather than rejections, my productivity tripled. The compiler is on your side. It’s catching the bug before production does.
One thing you can do this week
Take a small C or C++ function that manages memory (malloc/free, new/delete, a raw pointer, anything) and port it to Rust. Don’t worry about making it idiomatic. Just port it, run cargo check, and read every error message slowly.
You’ll get a handful of errors. Each one maps to a real class of bug in the C version. That’s the exercise that turned the borrow checker from an enemy into a colleague for me, and it took about an hour. An hour is cheap for a mental shift that sticks with you for years.
Rust isn’t going to be right for every project. But when memory safety is part of the job, the hour you spend fighting the compiler is the hour you didn’t spend fighting a segfault at 2am.