Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lifetimes: The Scope of Validity

Lifetimes have a reputation as the hard part of Rust, and the reputation is mostly a misunderstanding about what they are. The misunderstanding is that a lifetime annotation does something — that writing 'a creates or extends or manages the life of a value, the way new and free do in C.

It does not. Every reference in every Rust program already has a lifetime, whether or not anyone wrote it down, and that lifetime was determined by the structure of the code long before the annotation was typed. A lifetime parameter in a signature is a description: it tells the compiler how the lifetimes of the things going in relate to the lifetimes of the things coming out. It is closer to a type annotation than to a memory operation, and like a type annotation, it is often unnecessary because the compiler can work it out.

Most Rust code contains no lifetime annotations at all. The chapters you have read so far are full of references and free of 'a. What this chapter is about is the model underneath, so that when you do meet an annotation you can read it, and when the compiler tells you something does not live long enough, you can hear the design question in it.

What a lifetime actually is

A lifetime is a region of the program: the span during which a reference is guaranteed to point at something valid. The compiler computes one for every reference and checks a single property, which is that the region a reference is used in fits inside the region the referent exists in.

Here is that check failing:

fn main() {
    let outer;
    {
        let inner = String::from("temporary");
        outer = &inner;
    }
    println!("{outer}");
}

The compiler says inner does not live long enough. It is not being pedantic about a syntax rule. inner is dropped at the closing brace, its buffer is freed, and outer would be a pointer into freed memory — the C bug from chapter 3, arriving without a single unsafe or raw pointer in sight. Lifetime checking is that comparison, applied everywhere references travel.

Nothing in that example needed an annotation. The compiler could see both scopes. Annotations become necessary only at the one place where the compiler cannot see: a function boundary.

Why function signatures need help

A function is compiled once and called from anywhere. When the compiler checks a call site, it does not look inside the function body; it looks at the signature. So the signature has to carry enough information to answer the question the caller cares about, which is how long the returned reference stays valid.

Consider a function that returns whichever of two strings is longer. Written without annotations, it is rejected:

#![allow(unused)]
fn main() {
fn longer(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}
}

The compiler’s complaint is that it cannot tell whether the return value borrows from a or from b. That matters: if the result borrows from a, the caller may drop b immediately and keep using the result, and if it borrows from b, they may not. The body knows the answer is “either one, depending”. The signature has to say so:

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let title = String::from("the rust thought process");
    let subtitle = String::from("a book");
    println!("{}", longer(&title, &subtitle));
}

Read the signature as a sentence: for some region 'a, given two string slices that are both valid for 'a, I return a slice that is valid for 'a. Since the caller supplies the arguments, 'a ends up being the shorter of the two borrows, and the result cannot outlive either input. That is precisely the guarantee the body needs.

Now look at a variant that tells a different story:

fn with_prefix<'a>(text: &'a str, prefix: &str) -> &'a str {
    if text.starts_with(prefix) {
        &text[prefix.len()..]
    } else {
        text
    }
}

fn main() {
    let path = String::from("/usr/local/bin");
    let stripped;
    {
        let prefix = String::from("/usr");
        stripped = with_prefix(&path, &prefix);
    }
    println!("{stripped}");
}

Only text carries 'a. The signature says the result borrows from the first argument and has nothing to do with the second, which is why main can drop prefix and go on using the result. Had both parameters shared 'a, that program would not compile, and the restriction would be a fiction invented by a careless signature rather than a fact about the code.

This is the thing to take from the chapter. A lifetime annotation is a claim about which inputs the output borrows from, and getting it right is a design decision, not a syntax ritual. The compiler will accept an over-strict signature happily; your callers are the ones who pay.

The cases you never have to annotate

Rust applies three rules before asking you for anything, and between them they cover the overwhelming majority of functions.

Every elided input lifetime becomes its own distinct parameter. A function taking two references is treated as taking two unrelated ones.

If there is exactly one input lifetime, it is assigned to every output lifetime. This is why fn first_word(text: &str) -> &str from chapter 3 needs no annotation: there is only one place the result could have come from.

If one of the inputs is &self or &mut self, its lifetime is assigned to every output lifetime. This is why methods returning references almost never carry annotations — the answer is nearly always “it borrows from the receiver”, and the language assumes so.

When none of the three produces an unambiguous answer, the compiler stops and asks, which is what happened with longer. Treat that as the prompt to decide which input the result actually comes from.

Note

If you find yourself adding a lifetime parameter because the compiler asked and you are not sure which input is right, stop and work it out rather than trying 'a in different positions until it builds. A signature that compiles is not necessarily the signature you meant, and the wrong one becomes a constraint on every caller.

Types that borrow

A struct can hold a reference, and when it does, it takes a lifetime parameter:

struct Tokenizer<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Tokenizer<'a> {
    fn new(input: &'a str) -> Tokenizer<'a> {
        Tokenizer { input, position: 0 }
    }

    fn next_token(&mut self) -> Option<&'a str> {
        let rest = &self.input[self.position..];
        let start = rest.find(|c: char| !c.is_whitespace())?;
        let remainder = &rest[start..];
        let end = remainder.find(char::is_whitespace).unwrap_or(remainder.len());
        self.position += start + end;
        Some(&remainder[..end])
    }
}

fn main() {
    let text = String::from("ownership borrowing lifetimes");
    let mut tokenizer = Tokenizer::new(&text);
    while let Some(token) = tokenizer.next_token() {
        println!("{token}");
    }
}

Tokenizer<'a> is not a type that holds a string. It is a type that holds a view of a string owned by somebody else, and the 'a propagates that fact into the type system: a Tokenizer can never outlive the text it was built from, and the compiler enforces it at every place a Tokenizer is stored or returned.

Notice what this buys. The tokenizer allocates nothing. Every token it hands back points into the original buffer, and a program that tokenizes a hundred-megabyte file does no copying at all. This is the design that lifetime parameters exist to make safe, and it is why Rust’s parsers, serializers and text tools are as fast as they are.

When a borrowing type is the wrong answer

That same power is the most common way for a newcomer’s design to go wrong, and the mistake is worth naming clearly because it costs people weeks.

The shape is this. You write a struct that owns its data. Somewhere a borrow error appears. You notice that changing a String field to a &str field makes the error go away, so you add a lifetime parameter. Now the struct that contains that struct needs one too. Then the function returning it needs one, and the trait implementation will not accept one, and eventually you are threading 'a through eleven types to store a configuration value that is forty bytes long.

The diagnosis is that a lifetime parameter on a type is a statement that the type is a temporary view, and most types are not. Ask what the type is for. If it is created, used and dropped inside one function or one loop iteration — a parser over a buffer, an iterator, a formatter, a visitor walking a tree — then it is a view, and a lifetime is the right tool. If it is stored in a collection, returned up several layers, held across an await point, put in a struct that lives as long as the program, or sent to another thread, then it is storage, and it should own its data.

The cost of owning is an allocation and a copy. Compared to threading a lifetime parameter through a type hierarchy so that a config struct can avoid copying a path name once at startup, the allocation is free.

Coming from C++

The instinct that copying is expensive and must be avoided by default is correct in a language where a copy might be a deep copy of an object graph and a const& parameter is how you opt out. Rust moves rather than copying, so passing by value is already cheap, and String versus &str is a question about who owns the bytes rather than about how much work the call does. Reserve the borrowing design for the places where it removes real work — parsing, scanning, slicing large buffers — and let ordinary structs own what they hold.

The 'static misunderstanding

'static has two meanings that look identical and are not, and conflating them produces some of the most confusing error messages in the language.

As the lifetime of a reference, &'static str means the referent lives for the duration of the program. String literals are the everyday example: they are baked into the binary, so let name: &'static str = "anderix"; is fine.

As a bound on a generic parameter, T: 'static means something weaker and much more common: T contains no references with a lifetime shorter than the whole program. Every owned type satisfies it. String is 'static. Vec<u64> is 'static. A struct of owned fields is 'static. What is not 'static is Tokenizer<'a> from earlier, because it holds a borrow of something that might go away.

So when std::thread::spawn requires its closure to be 'static, it is not demanding that the closure run forever. It is saying that the closure may not hold a reference to a local variable in the spawning function, because that function may return while the thread is still running. Chapter 14 comes back to this. For now, read T: 'static as “owns its data” and the errors stop being mysterious.

Reading lifetime errors as design questions

Three messages account for most of what you will see, and each has a small set of sensible responses.

x does not live long enough means a reference is being kept past the life of its referent. Either the referent should live longer — declare it in an outer scope, or hoist it out of the loop — or the thing holding the reference should own its data instead.

Cannot return reference to local variable means a function is trying to hand out a view of something it is about to drop. The function should return the owned value, or take a destination to write into.

Missing lifetime specifier means a signature is ambiguous about which input the output borrows from. Decide which one, and if the answer is “neither, it’s something I built in here”, the function should return an owned value.

In all three, the productive move is the one chapter 1 described: read the error as a question about the design. The failing shapes are genuinely broken programs, and the compiler is naming the break.

Conclusion

A lifetime is the region in which a reference is valid, and the compiler computes it for every reference in the program. Annotations describe relationships across a function boundary, where the compiler can see only the signature. Three elision rules cover the ordinary cases, so most code carries no annotations at all.

A lifetime parameter on a type turns that type into a view: it cannot outlive what it borrows from, which is precisely what you want for parsers and iterators and precisely what you do not want for anything the program stores. When a lifetime parameter starts spreading through your types, that is evidence you asked a storage type to be a view.

The next chapter turns from references to the types themselves, and to how a Rust type is assembled out of data, behavior and the invariants you want it to guarantee.

Questions to think through

The with_prefix example gives 'a to one parameter and not the other. Write down what would break for a caller if both parameters shared 'a. Is anything in the function body different?

A Tokenizer<'a> cannot be stored in a struct that outlives its input. Find a type in a codebase you know that holds a pointer or reference to data it does not own, and ask what currently prevents that data from being freed first.

T: 'static is satisfied by every owned type. Why is that the right bound for thread::spawn, and what would go wrong if the bound were dropped?