People do not stall on lifetimes because the concept is hard. They stall
because the error text is opaque on first read, and the natural response —
shuffling 'a around until something changes — teaches nothing.
This is the decoder ring. Every snippet below was compiled on
rustc 1.95.0 --edition 2024; the codes and wording are what you will actually
see. Read it once now, and come back to it when something goes red.
E0106 — missing lifetime specifier
What it means: the elision rules could not work out the output lifetime, so the compiler is asking you a question.
fn get_str() -> &str { "hi" }
// ^ error[E0106]: missing lifetime specifier
// expected named lifetime parameter
fn frob(s: &str, t: &str) -> &str { s }
// ^ error[E0106]
struct Excerpt { part: &str }
// ^ error[E0106]
type MyStr = &str;
// ^ error[E0106]
How to answer it: work out where the returned reference’s bytes come from, and give that source’s lifetime to the output. If the answer is “from a local”, no annotation exists and you need a different design — see E0515.
This one gets its own article, because the shapes it comes in are the map of the whole topic.
E0515 — cannot return value referencing local variable
What it means: you tried to hand out a reference to something this function owns and is about to drop.
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
// error[E0515]: cannot return value referencing local variable `result`
// returns a value referencing data owned by the current function
}
How to answer it: you cannot, with an annotation. The caller chooses 'a;
a local cannot satisfy a promise the caller made. Return an owned value, or
restructure so the caller owns the storage. The fix menu gets a whole article.
E0597 — does not live long enough
What it means: a borrow is used at a point where its referent is already gone.
fn main() {
let r;
{
let x = 5;
r = &x;
// error[E0597]: `x` does not live long enough
// borrowed value does not live long enough
}
println!("{r}");
}
How to answer it: either shorten the borrow’s region (use it earlier) or
lengthen the referent’s life (declare it in an outer scope, or own it). This is
the “classic” lifetime error and the one whose fix is most often moving a
let.
E0716 — temporary value dropped while borrowed
What it means: the same thing as E0597, but the referent was a nameless
temporary, so there is no let to move.
fn bar(s: &String) -> &String { s }
fn foo() -> String { String::from("hi") }
fn main() {
let r = bar(&foo());
// error[E0716]: temporary value dropped while borrowed
// creates a temporary value which is freed while still in use
println!("{r}");
}
How to answer it: give the temporary a name. let owned = foo(); let r = bar(&owned); and the problem evaporates. Note that let r = &foo(); does
work — temporary lifetime extension applies to a direct &-of-temporary in a
let, but not once you route it through a function call. That asymmetry, and
what edition 2024 changed about it, gets its own article.
E0621 — explicit lifetime required in the type of x
What it means: you named some lifetimes but left the relevant parameter elided, and the body needs a relationship you did not declare.
fn foo<'a>(x: &'a str, y: &str) -> &'a str {
y
// error[E0621]: explicit lifetime required in the type of `y`
// lifetime `'a` required
}
How to answer it: decide whether you meant y: &'a str (tie them) or
meant to return something from x (fix the body). The compiler’s suggestion is
usually the first; the right answer is frequently the second.
E0505 — cannot move out because it is borrowed
fn main() {
let s = String::from("hi");
let r = &s;
let t = s;
// error[E0505]: cannot move out of `s` because it is borrowed
println!("{r} {t}");
}
How to answer it: the borrow is still live at the move because it is used
afterwards. Move the last use of r before the move, or clone.
E0499 and E0502 — the aliasing pair
fn main() {
let mut v = vec![1];
let a = &mut v;
let b = &mut v;
// error[E0499]: cannot borrow `v` as mutable more than once at a time
a.push(1);
b.push(2);
}
fn main() {
let mut v = vec![1];
let r = &v[0];
v.push(2);
// error[E0502]: cannot borrow `v` as mutable because it is also
// borrowed as immutable
println!("{r}");
}
How to answer them: these are aliasing errors, not lifetime-annotation errors, and no signature change fixes them. Shrink a region by moving a use, or restructure so the two accesses do not overlap. They appear in this list because a wrong lifetime annotation frequently causes them somewhere else — the zero-copy tokeniser problem in this track is exactly that story.
💡You get E0597 pointing at a local, and E0716 pointing at a temporary. Structurally, what is the same about them and what is different? click to reveal
Same: both say “a reference is required to be valid at a point where its referent has already been dropped.” They are one error about one rule.
Different: what the referent is, and therefore what fixes are available.
E0597’s referent is a named binding. You have a let to work with, so the fix menu includes moving that let outward, restructuring the block, or changing who owns the value. The compiler can point at the } where the drop happens.
E0716’s referent is an unnamed temporary created by an expression. There is no let to move, so the standard fix is to create one — bind the temporary to a name, and it becomes an E0597-shaped situation you already know how to solve. The other fix is to make the borrow shorter, since temporaries usually die at the end of the enclosing statement.
Recognising which one you have tells you which move to reach for, which is why the compiler bothers to have two codes.
Now the honest part: the hardest errors have no code
You will eventually hit this, and it is important that you know it is normal rather than a sign you have done something exotic:
error: lifetime may not live long enough
Bare. No E0nnn. Nothing to pass to rustc --explain. It is emitted by the
borrow checker’s region solver when the constraints are unsatisfiable and the
situation does not fit one of the named shapes above.
Two places it shows up constantly:
Explicitly named, over-constrained signatures:
fn longest<'a, 'b>(a: &'a str, b: &'b str) -> &'a str {
if a.len() >= b.len() { a } else { b }
// error: lifetime may not live long enough
// function was supposed to return data with lifetime `'a`
// but it is returning data with lifetime `'b`
// = help: consider adding the following bound: `'b: 'a`
}
Closures:
fn identity(x: &i32) -> &i32 { x } // fine
let identity = |x: &i32| x; // error: lifetime may not live long enough
Same body, same intent, and one of them has no error code. (Why closures behave differently from functions here is a genuine compiler wart, and it gets its own article.)
When you get a bare one, the useful move is to read the note: and help:
lines rather than the headline. They name the two regions and say which one
needed to outlive which. That sentence is the whole diagnosis.
💡rustc --explain E0759 returns something surprising. Why does this matter for anyone learning lifetimes from the internet?
click to reveal
It says: “Note: this error code is no longer emitted by the compiler.”
E0759 used to fire on things like fn foo(x: &i32) -> Box<dyn Debug> — a returned trait object silently defaulting to + 'static while the value inside borrows a short-lived reference. It has an unusually good explanation page, and it is all over Stack Overflow answers from 2020–2022.
It is also dead. Compile that function on 1.95 and you get the bare error: lifetime may not live long enough ... returning this value requires that '1 must outlive 'static. So a learner searching the error text they actually saw will find nothing, and a learner searching “E0759” will find good explanations of a code they will never see.
The lesson generalises beyond this one code. Rust’s diagnostics have improved a great deal, which means older answers describe messages that no longer exist. When an answer’s error text does not match yours, check whether it is stale before you assume you have a different problem. (E0477 is another retired one — rustc --explain E0477 says so too.)
One warning about the compiler’s advice
rustc’s help: suggestions are excellent for type errors and merely plausible
for lifetime errors. The most common bad outcome is being nudged toward
'static:
help: consider adding an explicit lifetime bound `T: 'static`
Sometimes that is right. Often the real fix is a shorter lifetime somewhere
else entirely, or an owned type, or moving a let. 'static makes the local
error go away and pushes an impossible requirement onto your callers, where it
will surface later as a worse error in code that looks unrelated.
Treat help: as a hypothesis. Read the note: lines — which name the actual
regions and the actual constraint — as the evidence.