Skip to content
← All articles

How to read a lifetime error

A decoder ring for the eight diagnostics you will actually meet — E0106, E0515, E0597, E0716, E0621, E0505, E0499, E0502 — each with a reproducer verified on rustc 1.95, plus the honest news that the hardest failures carry no error code at all.

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.