Guide contents

Lesson 27 of 94

Lifetime Elision & 'static

You have written dozens of functions that take and return references without any 'a. That's because the compiler applies three simple elision rules to fill in lifetimes you leave out. Knowing the rules tells you exactly when an annotation is needed, and why an error mentions a lifetime you never wrote.

The second half of this lesson covers 'static, the longest lifetime, and the misconceptions around it.

The three elision rules

The compiler fills in missing lifetimes in function and method signatures using these rules, in order:

  1. Each reference parameter gets its own lifetime. fn f(a: &str, b: &str) becomes fn f<'a, 'b>(a: &'a str, b: &'b str).
  2. If there is exactly one input lifetime, every output reference gets it. fn f(a: &str) -> &str becomes fn f<'a>(a: &'a str) -> &'a str.
  3. If one of the parameters is &self or &mut self, every output reference gets the lifetime of self.

If output lifetimes are still unknown after applying the rules, the compiler reports E0106: missing lifetime specifier and asks you to write them.

1. Rule 1: every input gets its own lifetime

This function returns nothing borrowed, so rule 1 is all it needs.

Rust
fn same_length(a: &str, b: &str) -> bool {
    a.len() == b.len()
}

// what the compiler sees:
fn same_length_explicit<'a, 'b>(a: &'a str, b: &'b str) -> bool {
    a.len() == b.len()
}

fn main() {
    println!("{} {}", same_length("rust", "java"), same_length_explicit("go", "java"));
}
Output
true false

2. Rule 2: one input lifetime flows to the output

Rust
fn trim_prefix(s: &str) -> &str {
    s.trim_start_matches("./")
}

// what the compiler sees:
fn trim_prefix_explicit<'a>(s: &'a str) -> &'a str {
    s.trim_start_matches("./")
}

fn main() {
    println!("{} {}", trim_prefix("./src/main.rs"), trim_prefix_explicit("././lib.rs"));
}
Output
src/main.rs lib.rs

3. Rule 2 also covers references inside other types

The single input lifetime flows into every reference in the output: tuples, Option, Vec, and structs with lifetimes.

Rust
fn split_extension(file: &str) -> Option<(&str, &str)> {
    file.rsplit_once('.')
}

fn words(text: &str) -> Vec<&str> {
    text.split_whitespace().collect()
}

fn main() {
    println!("{:?}", split_extension("archive.tar.gz"));
    println!("{:?}", split_extension("Makefile"));
    println!("{:?}", words("one two  three"));
}
Output
Some(("archive.tar", "gz"))
None
["one", "two", "three"]

4. Non-reference parameters don't count

Only references have lifetimes. A function with one reference and any number of owned or Copy parameters still has exactly one input lifetime, so rule 2 applies.

Rust
fn nth_word(text: &str, n: usize, uppercase_hint: bool) -> &str {
    let _ = uppercase_hint;
    text.split_whitespace().nth(n).unwrap_or("")
}

fn main() {
    println!("{}", nth_word("zero one two three", 2, false));
}
Output
two

5. Two input lifetimes: the rules give up

With two reference parameters, rule 2 does not apply and there is no self. The compiler has no way to know which input the output comes from.

Rust✗ does not compile · E0106
fn pick(primary: &str, fallback: &str) -> &str {
    if primary.is_empty() { fallback } else { primary }
}

fn main() {
    println!("{}", pick("", "default"));
}
Output
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:43
  |
1 | fn pick(primary: &str, fallback: &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 `primary` or `fallback`
help: consider introducing a named lifetime parameter
  |
1 | fn pick<'a>(primary: &'a str, fallback: &'a str) -> &'a str {
  |        ++++           ++                 ++          ++

The fix is the annotation from Lifetimes in Functions: fn pick<'a>(primary: &'a str, fallback: &'a str) -> &'a str.

6. Rule 3: methods return references tied to self

In a method, rule 3 gives every output the lifetime of &self, even when there are other reference parameters. This is right for the most common case, a getter.

Rust
struct Catalog {
    items: Vec<String>,
}

impl Catalog {
    // elided: fn find<'s, 'q>(&'s self, query: &'q str) -> Option<&'s String>
    fn find(&self, query: &str) -> Option<&String> {
        self.items.iter().find(|item| item.contains(query))
    }
}

fn main() {
    let c = Catalog { items: vec![String::from("red chair"), String::from("blue table")] };
    let found;
    {
        let q = String::from("table"); // shorter-lived query
        found = c.find(&q);
    }
    println!("{:?}", found); // fine: the result is tied to `c`, not to `q`
}
Output
Some("blue table")

7. When rule 3 guesses wrong

If a method returns a reference that comes from a parameter rather than from self, rule 3's guess is wrong, and the body fails to compile.

Rust✗ does not compile
struct Formatter {
    trim: bool,
}

impl Formatter {
    fn clean(&self, input: &str) -> &str {
        if self.trim { input.trim() } else { input }
    }
}

fn main() {
    let f = Formatter { trim: true };
    println!("{}", f.clean("  hi  "));
}
Output
error: lifetime may not live long enough
 --> src/main.rs:7:46
  |
6 |     fn clean(&self, input: &str) -> &str {
  |              -             - let's call the lifetime of this reference `'1`
  |              |
  |              let's call the lifetime of this reference `'2`
7 |         if self.trim { input.trim() } else { input }
  |                                              ^^^^^ method was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`
  |
help: consider introducing a named lifetime parameter and update trait if needed
  |
6 |     fn clean<'a>(&self, input: &'a str) -> &'a str {
  |             ++++                ++          ++

8. Fixing it with an explicit lifetime

Write a lifetime that connects the output to the parameter it really comes from.

Rust
struct Formatter {
    trim: bool,
}

impl Formatter {
    fn clean<'a>(&self, input: &'a str) -> &'a str {
        if self.trim { input.trim() } else { input }
    }
}

fn main() {
    let cleaned;
    {
        let f = Formatter { trim: true }; // the formatter can be short-lived
        cleaned = f.clean("  hello  ");
    }
    println!("[{cleaned}]");
}
Output
[hello]

9. The anonymous lifetime '_

'_ means "a lifetime I don't want to name; use elision". It is used in two places:

  • in types, to show that a struct borrows without naming the lifetime: Parser<'_>,
  • in impl blocks that never refer to the lifetime by name: impl Parser<'_>.
Rust
struct Parser<'a> {
    input: &'a str,
}

impl Parser<'_> {
    fn remaining(&self) -> usize {
        self.input.len()
    }
}

fn make_parser(text: &str) -> Parser<'_> {
    Parser { input: text.trim() }
}

fn describe(p: &Parser<'_>) -> String {
    format!("{} bytes left", p.remaining())
}

fn main() {
    let src = String::from("  let x = 1;  ");
    let p = make_parser(&src);
    println!("{}", describe(&p));
}
Output
10 bytes left

10. Elision in impl blocks with named lifetimes

When a method needs the struct's lifetime by name, keep impl<'a> Type<'a>. When it doesn't, impl Type<'_> is shorter. Both styles are common.

Rust
struct Tokens<'a> {
    parts: Vec<&'a str>,
}

impl<'a> Tokens<'a> {
    fn first(&self) -> Option<&'a str> {
        self.parts.first().copied()
    }
}

impl Tokens<'_> {
    fn count(&self) -> usize {
        self.parts.len()
    }
}

fn main() {
    let line = String::from("GET /health HTTP/1.1");
    let t = Tokens { parts: line.split(' ').collect() };
    println!("{:?} of {}", t.first(), t.count());
}
Output
Some("GET") of 3

Part 2: 'static

11. String literals are &'static str

'static is the lifetime of the entire program. String literals are stored in the compiled binary, so a reference to one is valid forever.

Rust
fn main() {
    let banner: &'static str = "Welcome to the app";
    let copy_of_ref;
    {
        let inner: &'static str = banner;
        copy_of_ref = inner;
    }
    println!("{copy_of_ref}");
}
Output
Welcome to the app

12. Returning &'static str from a function

A function that returns one of several literals can promise 'static, so its result can be stored anywhere without borrowing anything.

Rust
fn reason_phrase(status: u16) -> &'static str {
    match status {
        200 => "OK",
        201 => "Created",
        204 => "No Content",
        400 => "Bad Request",
        404 => "Not Found",
        500 => "Internal Server Error",
        _ => "Unknown",
    }
}

fn main() {
    let mut log: Vec<&'static str> = Vec::new();
    for code in [200, 404, 999] {
        log.push(reason_phrase(code));
    }
    println!("{:?}", log);
}
Output
["OK", "Not Found", "Unknown"]

13. static items give 'static references

A static variable lives for the whole program, so references to it are 'static.

Rust
static SUPPORTED_LOCALES: [&str; 4] = ["en-US", "en-GB", "de-DE", "ja-JP"];

fn default_locale() -> &'static str {
    SUPPORTED_LOCALES[0]
}

fn all_locales() -> &'static [&'static str] {
    &SUPPORTED_LOCALES
}

fn main() {
    println!("default: {}", default_locale());
    println!("all: {:?}", all_locales());
}
Output
default: en-US
all: ["en-US", "en-GB", "de-DE", "ja-JP"]

14. Static lookup tables

Tables of constant data are a natural use of 'static: no allocation, no initialization at runtime, and shareable everywhere.

Rust
static COUNTRY_CODES: &[(&str, &str, u16)] = &[
    ("US", "United States", 1),
    ("GB", "United Kingdom", 44),
    ("DE", "Germany", 49),
    ("JP", "Japan", 81),
    ("BR", "Brazil", 55),
];

fn country(code: &str) -> Option<(&'static str, u16)> {
    COUNTRY_CODES
        .iter()
        .find(|(c, _, _)| c.eq_ignore_ascii_case(code))
        .map(|(_, name, dial)| (*name, *dial))
}

fn main() {
    for code in ["de", "JP", "xx"] {
        match country(code) {
            Some((name, dial)) => println!("{code}: {name} (+{dial})"),
            None => println!("{code}: unknown"),
        }
    }
}
Output
de: Germany (+49)
JP: Japan (+81)
xx: unknown

15. Structs with 'static fields

A struct can hold &'static str fields without any lifetime parameter, because 'static is a specific lifetime, not a parameter. This is common for built-in error codes, command definitions, and metadata.

Rust
#[derive(Debug)]
struct ErrorCode {
    code: &'static str,
    message: &'static str,
    http_status: u16,
}

const NOT_FOUND: ErrorCode = ErrorCode { code: "E404", message: "resource not found", http_status: 404 };
const RATE_LIMITED: ErrorCode = ErrorCode { code: "E429", message: "too many requests", http_status: 429 };

fn main() {
    for e in [NOT_FOUND, RATE_LIMITED] {
        println!("{} ({}): {}", e.code, e.http_status, e.message);
    }
}
Output
E404 (404): resource not found
E429 (429): too many requests

16. 'static does not fix borrow errors

When a borrow error appears, it is tempting to add 'static to the signature. That only makes the requirement stricter: now the argument must live for the whole program, and ordinary Strings don't.

Rust✗ does not compile · E0597
fn remember(s: &'static str) -> &'static str {
    s
}

fn main() {
    let name = String::from("Ferris");
    let kept = remember(&name);
    println!("{kept}");
}
Output
error[E0597]: `name` does not live long enough
 --> src/main.rs:7:25
  |
6 |     let name = String::from("Ferris");
  |         ---- binding `name` declared here
7 |     let kept = remember(&name);
  |                ---------^^^^^-
  |                |        |
  |                |        borrowed value does not live long enough
  |                argument requires that `name` is borrowed for `'static`
8 |     println!("{kept}");
9 | }
  | - `name` dropped here while still borrowed

The compiler's help: consider using the 'static lifetime messages are occasionally right (for returning literals), but most of the time the real fix is ownership: return a String, or keep the data alive longer.

17. An owned String is not &'static str, but it can outlive everything

A common confusion: an owned String is not 'static data, but it does not borrow anything either, so it is free to live as long as its owner wants. For storing text long-term, String is almost always the answer, not &'static str.

Rust
fn build_banner(user: &str) -> String {
    format!("*** Welcome, {user}! ***")
}

fn main() {
    let banner: String;
    {
        let user = String::from("ferris");
        banner = build_banner(&user);
    }
    println!("{banner}"); // owns its text: no lifetime involved
}
Output
*** Welcome, ferris! ***

18. Leaking memory to get a &'static

Box::leak deliberately never frees a heap value, and in return gives a &'static reference to it. It is occasionally useful for data loaded once at startup and used for the rest of the program. Use it sparingly: leaked memory is never reclaimed.

Rust
fn load_config() -> &'static str {
    let text = String::from("mode=production\nregion=eu-west-1"); // pretend this was read from a file
    Box::leak(text.into_boxed_str())
}

fn main() {
    let config: &'static str = load_config();
    let mode = config.lines().next().unwrap();
    println!("{mode}");
}
Output
mode=production

into_boxed_str() converts the String to a Box<str>, and Box is covered in Box.

19. const vs static

Both give program-long values. A const is inlined at every use and has no fixed address; a static is a single location in memory. For &'static str values, either works; for large tables, prefer static so the data exists only once.

Rust
const GREETING: &str = "hello";
static FAREWELL: &str = "goodbye";

fn main() {
    let a: &'static str = GREETING;
    let b: &'static str = FAREWELL;
    println!("{a} / {b}");
    println!("same address every time: {}", std::ptr::eq(&FAREWELL, &FAREWELL));
}
Output
hello / goodbye
same address every time: true

20. Reading lifetime error messages

Errors that mention lifetimes follow a few standard shapes. Recognizing them saves a lot of time:

Message What it means Usual fix
missing lifetime specifier (E0106) Elision couldn't decide what an output borrows from Add <'a> and tie the output to the right input
borrowed value does not live long enough (E0597) A reference is used after its value was dropped Keep the value alive longer, or own the data
cannot return reference to local variable (E0515) A function returns a reference to its own local Return the owned value
lifetime may not live long enough The body returns something the signature did not allow Fix the signature, or return something else
explicit lifetime required in the type of x (E0621) A parameter needs the output's lifetime Add 'a to that parameter
temporary value dropped while borrowed (E0716) A reference points into a temporary expression Bind the temporary with let first

The next example shows the last one, which surprises everyone once.

21. Temporaries are dropped at the end of the statement

A value created in the middle of an expression, such as String::from(...), is a temporary. It is dropped at the end of the statement unless it is bound to a variable.

Rust✗ does not compile · E0716
fn main() {
    let name: &str = String::from("  Ferris  ").trim();
    println!("{name}");
}
Output
error[E0716]: temporary value dropped while borrowed
 --> src/main.rs:2:22
  |
2 |     let name: &str = String::from("  Ferris  ").trim();
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^       - temporary value is freed at the end of this statement
  |                      |
  |                      creates a temporary value which is freed while still in use
3 |     println!("{name}");
  |                ---- borrow later used here
  |
help: consider using a `let` binding to create a longer lived value
  |
2 ~     let binding = String::from("  Ferris  ");
3 ~     let name: &str = binding.trim();
  |
Rust
fn main() {
    let owned = String::from("  Ferris  "); // bind the temporary...
    let name: &str = owned.trim();           // ...then borrow from it
    println!("{name}");
}
Output
Ferris

22. Realistic example: a command table with 'static metadata

A command-line tool's commands are known at compile time. Storing their names and help texts as &'static str costs nothing at runtime.

Rust
struct Command {
    name: &'static str,
    aliases: &'static [&'static str],
    help: &'static str,
}

static COMMANDS: &[Command] = &[
    Command { name: "build", aliases: &["b"], help: "compile the project" },
    Command { name: "test", aliases: &["t"], help: "run the tests" },
    Command { name: "clean", aliases: &[], help: "remove build output" },
];

fn resolve(input: &str) -> Option<&'static Command> {
    COMMANDS.iter().find(|c| c.name == input || c.aliases.contains(&input))
}

fn main() {
    for input in ["b", "test", "deploy"] {
        match resolve(input) {
            Some(c) => println!("{input} -> {}: {}", c.name, c.help),
            None => println!("{input} -> unknown command"),
        }
    }
}
Output
b -> build: compile the project
test -> test: run the tests
deploy -> unknown command

23. Realistic example: MIME types with an elided lookup

The function takes a borrowed file name and returns a &'static str. The two lifetimes are unrelated: the result does not borrow the input at all, which is exactly what the signature says.

Rust
static MIME_TYPES: &[(&str, &str)] = &[
    ("html", "text/html"),
    ("css", "text/css"),
    ("js", "text/javascript"),
    ("json", "application/json"),
    ("png", "image/png"),
    ("svg", "image/svg+xml"),
];

fn mime_type(file_name: &str) -> &'static str {
    let ext = file_name.rsplit_once('.').map(|(_, e)| e).unwrap_or("");
    MIME_TYPES
        .iter()
        .find(|(e, _)| e.eq_ignore_ascii_case(ext))
        .map(|(_, m)| *m)
        .unwrap_or("application/octet-stream")
}

fn main() {
    let mut served = Vec::new();
    for name in ["index.HTML", "app.js", "logo.svg", "data.bin"] {
        let owned_name = name.to_string();
        served.push(mime_type(&owned_name)); // `owned_name` is dropped each iteration; the result isn't tied to it
    }
    println!("{:?}", served);
}
Output
["text/html", "text/javascript", "image/svg+xml", "application/octet-stream"]

Summary

  • Rule 1: each reference parameter gets its own lifetime.
  • Rule 2: with exactly one input lifetime, outputs get it.
  • Rule 3: in methods, outputs get the lifetime of &self.
  • If outputs are still ambiguous, you write the lifetimes (E0106). If rule 3 guesses wrong, name the lifetime of the parameter the result really comes from.
  • '_ means "elided here", in types (Parser<'_>) and impl blocks.
  • 'static is the lifetime of the whole program: string literals, static items, and leaked data.
  • Adding 'static to a signature makes it stricter, and almost never fixes a borrow error. Owned data is the usual fix.

Next: Fixing Borrow Checker Errors, a clinic of the errors you will meet most, with a fix for each.