The bug you now know how to cause
By the time you reach this rung you can do something most beginners cannot: you can name, precisely, how a C program corrupts itself. You know that calling free() and then reading through the same pointer is a use-after-free; that forgetting free() is a memory leak; that writing past the end of an array is a buffer overflow; and that all of these are undefined behavior, which the optimizer may turn into anything at all. You also know the uncomfortable truth from the last rung — these are not exotic mistakes. They are the default failure mode of careful, experienced people writing ordinary code.
These bugs share one root, and it is worth saying plainly. In C, the question "who is responsible for this block of memory, and is it still valid right now?" has no answer the language tracks. It lives in your head, in comments, in conventions across a team. The compiler will happily let you free a block twice, hand out a pointer to a local that has already gone out of scope, or index an array with a number you never checked. Nothing in the type of `char *s` records whether s still points at something alive. That missing information is exactly the gap every memory bug falls through.
Two old answers, and the cost of each
Before Rust, the industry had two main answers to memory bugs, and each buys safety with a real price. The first answer is manual discipline — C and C++ — where you, the programmer, hold the rules in your head and the language trusts you completely. The price is the entire last rung of this ladder: a single slip becomes silent corruption. The reward is total control and no hidden overhead; nothing runs that you did not write.
The second answer is a garbage collector — Java, Go, Python, C#, JavaScript. Here the language tracks, at run time, which blocks of memory are still reachable, and automatically frees the rest. This is the alternative C deliberately lacks, and it genuinely abolishes use-after-free and double-free: you never call free() at all, so you cannot get it wrong. The price is paid in a different currency. A garbage collector needs a runtime that periodically scans live memory, which costs CPU time and can introduce pauses; it usually keeps more memory live than strictly necessary; and you lose the deterministic, predictable cleanup that systems code — a kernel, a database, an embedded controller — often cannot do without.
So for decades the choice felt forced: either manual control with manual danger, or automatic safety with a runtime tax you cannot opt out of. Systems programmers, who care about exactly the predictability a garbage collector takes away, mostly stayed with C and lived with the bugs. Rust's whole reason to exist — its motivation — is the claim that this is a false choice, and that you can have memory safety and no garbage collector and C-level control, all at once. That claim sounds too good, so the honest question is: where is the catch?
Where the catch is: the compiler does the bookkeeping
Rust's trick is to move the bookkeeping you used to do in your head into the type system, and to do it entirely at compile time. The missing information from earlier — who owns this block, and is it still valid? — becomes a property the compiler actually tracks. The central idea is ownership: every value has exactly one owner, and when the owner goes out of scope, the value is freed automatically. There is no garbage collector scanning at run time; the compiler simply knows, at the point you wrote the code, where each value's life ends, and inserts the equivalent of free() there for you.
Two more rules close the gaps that ownership alone leaves. Move semantics (moves) say that when you assign or pass a value, ownership moves to the new place and the old name can no longer be used — so two variables never both think they own the same block, which is what made double-free possible in C. And borrowing (borrowing) lets you lend out temporary access — a reference — without giving up ownership, under rules the borrow checker enforces: you may have many read-only borrows, or exactly one writable borrow, but never both at once. That single rule, checked at compile time, is what makes data races and use-after-free unrepresentable in safe Rust. We will spend the next two guides entirely on ownership/moves and on borrowing; here you only need the shape.
C: who frees this? you must remember.
char *s = malloc(64); // you own it... by convention
use(s);
free(s); // forget this -> leak
use(s); // do this after free -> use-after-free (UB)
Rust: the compiler tracks the owner and frees at end of scope.
let s = String::from("hi"); // s owns the heap buffer
use_it(s); // ownership MOVES into use_it
// s is no longer usable here -- the compiler rejects use(s)
// when the owner goes out of scope, the buffer is freed: no free() call,
// no GC, no double-free, no use-after-free -- all proven at compile timeReferences and lifetimes, and nulls that cannot bite
A Rust reference is, at the machine level, just a pointer — the same address in a register you have known all along. What is different is that every reference carries an invisible second fact the compiler reasons about: its lifetime, the span during which the thing it points to is guaranteed to stay alive. The borrow checker uses lifetimes to prove that no reference ever outlives its target, which is precisely the dangling pointer you learned to fear in C. If you try to return a reference to a local that is about to be destroyed, the program does not compile. The pointer is the same; the difference is that the compiler refuses to let it become dangling.
Rust also closes the other great C wound, the one that is not about memory blocks at all: the null pointer and the unchecked error. There is no null in safe Rust. Where C uses NULL to mean "nothing here," Rust uses Option, a type that is either a real value or an explicit None — and the compiler will not let you use the value without first checking which it is. Where C returns -1 and sets errno and hopes you check it, Rust returns a Result that is either a success or an error, again forcing you to handle both. The fourth guide in this rung is devoted to these. The pattern is the same one you have seen twice now: information that was a convention in C becomes a fact the type system enforces.
Notice that none of this needs a runtime. There is no scanner, no pause, no hidden thread. Every one of these checks happens once, in the compiler, and then disappears: the binary Rust produces is bare machine code with no more overhead than the equivalent C. This is what people mean by the phrase "zero-cost" — the safety is paid for in compile time and in your own effort wrestling the borrow checker, not in run-time speed. That is the real shape of Rust's bargain, and it is genuinely different from a garbage collector's.
The honest catch: a curve, an escape hatch, not magic
Now the part the marketing skips. The borrow checker is strict, and being strict means it has false positives: it will reject code that you, looking at it, can see is perfectly safe. A pattern that is obviously fine to a human can violate the one-writer-or-many-readers rule as the checker understands it, and you will be told no. This is not a bug; it is the price of a checker that can never let a real violation through. Every experienced Rust programmer has fought the borrow checker over correct-looking code, and the early months of learning Rust are largely the months of internalizing what it will and will not accept. Anyone who tells you the curve is gentle is selling something.
There is also an escape hatch, and it must be named honestly: the unsafe keyword. Some real things — dereferencing a raw pointer, calling into a C library, writing a data structure the borrow checker cannot prove — genuinely cannot be expressed in safe Rust, so Rust gives you unsafe blocks where those operations are allowed and the compiler stops vouching for them. Inside unsafe, you are back to C-level responsibility; you can cause undefined behavior again. The crucial design point is that unsafe is bounded: it is a small, greppable, explicitly marked region, so when a memory bug does appear, the suspects are a handful of unsafe blocks rather than the whole program. That is a profoundly different debugging story from C, but it is not the absence of danger.
What your C knowledge buys, and where we go next
Here is the encouraging truth to carry up the rest of this rung: learning C was not a detour you now have to unlearn. Rust runs on the very same machine — the same stack and heap, the same pointers underneath references, the same notion that a value occupies bytes at an address. Everything you built about memory layout, ownership of resources, and how a program actually executes is the exact mental model Rust assumes you already have. C taught you why these rules matter by letting you break them; Rust takes the same rules and asks a compiler to enforce them. You are not starting over; you are getting a checker for the discipline you already practice.
One sharp way to see the whole shift: in C, memory safety and type safety are things you hope to maintain by being careful; in Rust they are things the compiler refuses to let you violate in safe code. The cost is a stricter compiler and a real learning curve; the gain is that an entire category of 3 a.m. production crashes simply cannot happen in the safe part of your program. Whether that trade is worth it depends on what you are building — and now you have enough of the real picture to judge for yourself, rather than on hype.
From here the rung gets concrete. The next guide takes ownership and move semantics apart in full — what "move" really does to the bytes, why a moved-from value is unusable, and how the owner's exit triggers cleanup. After that comes borrowing and the borrow checker in depth, then Option and Result for nulls and errors by value, and finally Cargo together with a translation table mapping each C concept onto its Rust counterpart. You arrived at this rung already understanding the disease; the rest of it is a careful tour of the cure and its side effects.