Past the Borrow Checker
“The borrow checker is the easy part.” — every engineer who has fought
for<'a>for an afternoon
There is a moment, somewhere between the second month and the second year of writing Rust, when the language stops being the language you read about and starts being a different language. You have made peace with &mut. You no longer fight ownership; you think in it. You read a function signature and your eye lands on the return type and the lifetimes feel routine, like punctuation. You have shipped real code. You like Rust.
Then you write your first non-trivial async function that returns a future borrowing from self, and the compiler tells you something like:
error: lifetime may not live long enough
--> src/lib.rs:12:9
|
9 | async fn handle<'a>(&'a self, req: &'a Request) -> Response {
| -- ----- has type `&'a Server`
| |
| lifetime `'a` defined here
...
12 | self.dispatch(req).await
| ^^^^^^^^^^^^^^^^^^^^^^^^ requires that `'a` must outlive `'static`
And you read it. And you read it again. And the words on the screen are individually English words, and they form sentences with subjects and verbs, and you understand none of it, because the question you want answered — what do I change — is not in there. The compiler is telling you about a constraint between two things. It is not telling you which thing to move.
This is the wall. Welcome.
What this book assumes
You have read The Rust Programming Language. Probably twice. You have written real Rust — not toys, real code, the kind that sits in a service and handles requests or in a binary that someone else runs. You know what &mut means and why you can’t have two of them. You know what Box<dyn Trait> does. You can read a where T: 'a + Send + Sync clause without flinching. You probably know what Cow is and have used it at least once on purpose.
You are not a beginner. You are an engineer who has bought into Rust, and now Rust is asking more of you than you signed up for.
This book starts at that point. It does not explain ownership. It does not explain match. It does not show you how to call a function. If you need those things, the official book is excellent and free and waiting for you. Come back when you’ve hit the wall.
What the wall looks like
The wall has four corners. They are:
-
Lifetimes that don’t behave. Specifically, lifetimes that involve closures, or trait objects, or
async, or any combination thereof. The simple'aparameters you got used to from chapter 10 of the official book have an entire algebra you didn’t know existed, and the algebra is what’s failing. -
Variance. A property of generic type parameters that nobody told you about, that the compiler knows by heart, that determines whether
Vec<&'static str>can be used whereVec<&'a str>is expected, and whose precise rules are why some of your error messages mentionsubtypeandsupertypewhen you weren’t using inheritance. -
Higher-rank trait bounds. The
for<'a>syntax. The thing that shows up in error messages about closures the moment you try to write a callback that takes a reference. The single language feature most likely to make you wonder if you should switch to Go. -
Pin, and the entire async machinery sitting on top of it. A type that, if you look at it for the first time without context, appears to do nothing. It is, in fact, doing the most subtle thing in the standard library, and once you understand why, you will respect it deeply, and once you have to use it in anger, you will resent it.
These four corners are not independent. The reason async is hard is that it touches all three of the others. The reason Pin exists is that the async desugaring would otherwise be unsound. The reason your async closure won’t compile is that HRTBs and lifetime variance interact in ways the inference engine cannot always figure out. The reason your error message mentions 'static is that the compiler has decided your borrow has to outlive a future whose lifetime it cannot bound.
Everything is connected. That is part of why this is hard. We will take the corners one at a time anyway.
Why the official docs stop being enough
The official Rust documentation — the book, the reference, the rustonomicon — is excellent. It is also written for an audience that is mostly trying to avoid this material. The book stops at lifetimes-as-scopes. The reference will tell you formally what variance is, but it will not tell you why your code triggers it. The rustonomicon assumes you are writing unsafe, which means it skips the parts of safe Rust that are nevertheless impossible to use without the same depth of understanding.
The async book exists, and it is good, and it will not save you from a real-world Send bound failure across an await point inside a closure inside a tokio::select!. Nothing currently in print will save you from that. You have to build the model yourself, and once it’s built, the error message becomes legible in roughly the same way git rebase --interactive becomes legible after you’ve ruined a branch with it three times.
This book is the thing that wishes it had existed for those three times.
How this book is organized
Part I — The Type System’s Edges. Lifetimes (chapter 1), variance (chapter 2), and HRTB (chapter 3). The three concepts that, together, form the substrate everything else in this book is going to need. Read in order.
Part II — Async, From the Inside. What async fn desugars to (chapter 4), why holding a reference across await is fundamentally different from holding it across a function boundary (chapter 5), Pin and the soundness problem it solves (chapter 6), and the long unhappy story of async fn in traits (chapter 7).
Part III — Survival. Real compiler errors decoded line by line (chapter 8), and the small handful of patterns that cover most of what you’ll actually do in production (chapter 9).
Part IV — Disagreement. When the compiler is right and you are wrong (chapter 10), and when the compiler is wrong and you are right and what you do about it (chapter 11).
There is also a bibliography, which is short, because the writing in this space is mostly blog posts and RFCs, and they are linked there.
A note on tone
I am going to be sympathetic to your suffering and direct about the difficulty. The reason is that I respect you. You are not a beginner being eased in. You have already done the easy part. You have already invested. You deserve someone to acknowledge that the next part is genuinely hard, that the design choices that make it hard are mostly defensible, and that the path through is built out of a small number of mental models you can actually learn.
I will, occasionally, point out where the language design has been controversial. The async-trait situation in particular has burned several years of community attention and is not fully resolved as of 2026. I will not pretend it is. The Rust project’s conduct in working through these problems has been, on the whole, admirable; the result, on the whole, has been imperfect. Both can be true.
Let’s begin.