Lesson 8.5 · 12 min
How long a borrow lasts
Back in lesson 4.6, first_word handed back a window into your message, and the compiler somehow knew that window was safe to use. This lesson answers how. The answer has a name, lifetimes, and most of the time you never write one.
Adds to ChatterboxNothing new: you learn why first_word compiles.
Compiler errors you will meet E0106
The question first_word left open
Chatterbox's command.rs still has the function from lesson 4.6: fn first_word(text: &str) -> &str. It does not copy anything. It hands back a borrow: a window onto the first few bytes of text that somebody else owns. A window is only useful while the thing behind it still exists. So how does the compiler know, everywhere first_word is called, that the text is still there when the window is used?
Start with the danger itself, in a tiny program. The curly braces in the middle make an inner block: anything made inside it is cleaned up at its closing brace, exactly like a function's frame in lesson 4.1.
What happens when this program is compiled?
- It compiles and runs
- Compile error E0597
- It compiles, then panics
Reveal the answer
It does not compile: E0597. The text msg owns is cleaned up at the closing brace on line 6, but r is used on line 7. A borrow that outlives its owner would point at freed memory, so the compiler refuses to build the program at all.
error[E0597]: `msg` does not live long enough --> src/main.rs:5:13 | 4 | let msg = String::from("hello"); | --- binding `msg` declared here 5 | r = &msg; | ^^^^ borrowed value does not live long enough 6 | } | - `msg` dropped here while still borrowed 7 | println!("{r}"); | - borrow later used here
Step through the program. Solid arrows are ownership; dashed arrows are borrows. Use ← → or the slider.
- Line 1. The program starts in main. Line 2 announces r, but r holds nothing yet, so it gets no row until line 5.
- Line 4. Inside the inner block, msg is made. Its text lives on the heap and msg holds the one key.
- Line 5. r borrows msg: the dashed arrow is a loan, not a key.
r:&Stringborrowsmsg. - Line 6. The inner block ends. msg was made inside it, so msg goes away and its text is freed. r still lives, because it was made outside the block, and its arrow now points at memory that holds nothing.
- Line 7. Printing r would read freed memory: a dangling reference. The compiler stops the build here with E0597, so this picture never happens on a real machine.
A lifetime is a stretch of code
In languages without this check, a reference like r is a classic bug: the program reads memory that has been handed back and maybe reused, and prints garbage, crashes, or worse. Rust rules it out before the program exists.
To do that, the compiler gives every borrow a lifetime: the stretch of the program during which the borrow is good. Here it is from line 5 to the last use on line 7. The owner msg only lives until line 6. The rule is one sentence: a borrow's lifetime must fit inside its owner's. Line 7 breaks it, and that is E0597.
The same rule explains a mistake everyone makes once: making a value inside a function and handing back a borrow of it. When the function returns, its frame is gone, and the value with it.
error[E0515]: cannot return reference to local variable `text` --> src/main.rs:3:5 | 3 | &text | ^^^^^ returns a reference to data owned by the current function
Why first_word needs nothing written
Here is the catch. The compiler checks each function on its own, by its first line, its signature. When it checks a call to first_word, it does not read the body. So the signature has to say where the returned borrow comes from. For most functions there is only one sensible answer, and Rust fills it in for you. This is called lifetime elision (elided means "left out"), and it follows three rules:
- Every borrowed input gets its own lifetime.
- If there is exactly one borrowed input, the output borrows from it.
- If the function is a method with
&self, the output borrows fromself.
first_word(text: &str) -> &str has one borrowed input, so rule 2 says the result borrows from text, and every caller is checked against that. Chatterbox's arg works the same way. And the name(&self) -> &str method you wrote for Replier in lesson 8.4 is rule 3.
The rules run out when there is no single answer. Then the compiler asks you.
This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.
error[E0106]: missing lifetime specifier --> src/main.rs:1:32 | 1 | fn longer(a: &str, b: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b` help: consider introducing a named lifetime parameter | 1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str { | ++++ ++ ++ ++
E0106 in the compiler error atlas →
missing lifetime specifier: the output is a borrow, and the compiler cannot tell how long it is good for.the signature does not say whether it is borrowed from `a` or `b`: two borrowed inputs, so rule 2 does not apply. The body picks one at run time; the signature has to cover both.consider introducing a named lifetime parameter: the compiler suggests the exact fix: a name,'a, on both inputs and the output.
Fixes that were compiled for real
- Name one lifetime for all three — compiles. <code><'a></code> introduces the name, like <code><T></code> introduced a type in lesson 8.3. It reads: both inputs are good for at least 'a, and so is the result.
- Return an owned String instead — compiles, with a cost. No borrow, so nothing to describe. It copies the text, which is fine for a short message.
- Trap: tie the result to a only — still E0621. Now the signature promises the result comes from <code>a</code>, and the body can return <code>b</code>. The compiler holds the body to its own signature.
Notice what 'a does not do: it does not make anything live longer. A lifetime annotation only describes how borrows relate, so the compiler can check each call. With one 'a on both inputs, the result is good only while both are, which is the shorter of the two. Try it.
first is longer, so longer returns first, which lives to the end. What does the compiler say?
- It compiles and runs
- Compile error E0597
- Compile error E0106
Reveal the answer
It does not compile: E0597. The compiler does not run your program to see which one comes back. The signature says the result may be either input, so it is only good while both are, and second is gone after line 11.
error[E0597]: `second` does not live long enough --> src/main.rs:10:33 | 9 | let second = String::from("hi"); | ------ binding `second` declared here 10 | result = longer(&first, &second); | ^^^^^^^ borrowed value does not live long enough 11 | } | - `second` dropped here while still borrowed 12 | println!("{result}"); | ------ borrow later used here
Each bar is one borrow, drawn from its first use to its last. Scrub the line to see what is alive.
first: owner — owner, lines 6–13first: result may borrow this — shared borrow, lines 10–12second: owner (inner block) — owner, lines 9–11second: result may borrow this — shared borrow, lines 10–12
The top row is fine: the loan ends inside its owner. In the bottom row the loan runs past line 11, where second is dropped. Moving the println! inside the block, or making second before the block, fixes it.
Text that lasts forever: 'static
One lifetime has a name built in. Text in quotes, like "/help", is written into the program file itself, so it exists for the whole run. Its type is &'static str: a borrow good for as long as the program runs. Chatterbox's const HELP: &str is one of those; inside a const, Rust fills in 'static for you. In a function with no borrowed input at all, there is nothing to borrow from, so you must say it.
This function takes nothing and returns text in quotes. What does the compiler say?
- It compiles and runs
- Compile error E0106
- Compile error E0515
Reveal the answer
It does not compile: E0106. With no borrowed input, no elision rule applies, so the output's lifetime is missing. The compiler suggests &'static str, which is right here: the text is part of the program. It also suggests returning a String, which is usually right when the text is built at run time.
error[E0106]: missing lifetime specifier --> src/main.rs:1:18 | 1 | fn bot_name() -> &str { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static` | 1 | fn bot_name() -> &'static str { | +++++++ help: instead, you are more likely to want to return an owned value | 1 - fn bot_name() -> &str { 1 + fn bot_name() -> String { |
A stand-alone function Chatterbox could use for a smarter /find: return the first message in history that contains word, as a borrow of that message, or None. The starter does not compile: run it and read the E0106. Decide which input the answer borrows from, write the signature, then the body. One hidden test drops the word before it uses the answer.
A stand-alone function Chatterbox could use for a smarter /find: return the first message in history that contains word, as a borrow of that message, or None. The starter does not compile: run it and read the E0106. Decide which input the answer borrows from, write the signature, then the body. One hidden test drops the word before it uses the answer.
Hidden tests (4): an_empty_history_is_none finds_the_first_mention a_word_never_said_is_none the_word_can_go_before_the_answer_is_used
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
If you have coded before
In Python a value stays alive as long as anything refers to it, and a garbage collector frees it later, so a dangling reference cannot happen, at the cost of that collector running alongside your program. In C, nothing stops one. Rust gets Python's safety without a collector by checking lifetimes while compiling, so the finished program carries no extra work.
What you have now
Chatterbox did not change in this lesson; you can now read it more closely. Every & in it has a lifetime, and the compiler checked each one fits inside its owner. Elision wrote them for first_word, arg and Replier::name, and you write one only when a function returns a borrow and the rules cannot tell where it comes from. As a preview, chapter 9 adds JSON and the real model.
Say it out loud, then reveal
What is a lifetime?Reveal
Why does <code>fn first_word(text: &str) -> &str</code> need no annotation?Reveal
What does <code>fn longer<'a>(a: &'a str, b: &'a str) -> &'a str</code> promise the caller?Reveal
What is <code>&'static str</code>?Reveal
Your Chatterbox so far: v0.8 (after lesson 8.4)
Download this version (zip)Unzip, then cargo run inside the folder.
That is lesson 8.5. Tick it off so the course remembers where you are.
Go deeper
Questions people ask
What are lifetimes in Rust?
A lifetime is the part of the program during which a reference is valid. The compiler checks that no reference is used after the value it points at is gone. Most lifetimes are worked out for you; you write one, like the a in longer, when a function returns a reference and the compiler cannot tell which input it comes from.
What does missing lifetime specifier mean?
It is error E0106. A function returns a reference, but its signature does not say which input that reference borrows from, usually because there are two reference inputs, or none. Name a lifetime and put it on the output and the input it comes from, or return an owned value such as a String.
Do lifetime annotations make a value live longer?
No. They only describe how long references are valid relative to each other so the compiler can check every call. How long a value lives is still decided by its owner and its scope.
What is the static lifetime?
It means the reference is valid for the entire run of the program. Text written in quotes in your code has the static lifetime because it is stored in the program file itself.