ZetCode

Rust lifetimes

last modified October 25, 2026

In this article we explain lifetimes in Rust.

A lifetime is the region of code in which a reference is valid. Every reference in Rust has a lifetime. The compiler's borrow checker compares lifetimes to make sure that no reference outlives the data it points to. Such a reference would be a dangling reference, a classic source of crashes and security bugs in C and C++.

Most of the time lifetimes are inferred and we never write them. We have to annotate them when the compiler cannot figure out how the lifetimes of several references relate to each other. Lifetime annotations start with an apostrophe, such as 'a. They do not change how long any value lives; they describe relationships that the compiler then checks. Ownership and borrowing, on which lifetimes build, are covered in Rust ownership.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q. Several examples do not compile on purpose; for them we show the compiler error.

Dangling references

The main purpose of lifetimes is to prevent dangling references.

main.rs
fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}

The variable r is declared in the outer scope and assigned a reference to x in the inner scope.

{
    let x = 5;
    r = &x;
}

The variable x is dropped at the end of the inner block, but r is used after it. The reference would point to freed memory. The borrow checker sees that the lifetime of r is longer than that of x and rejects the program.

$ cargo run -q
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                   - borrow later used here

For more information about this error, try `rustc --explain E0597`.

Lifetimes in function signatures

A function that returns a reference must tell the compiler which input the reference comes from.

main.rs
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let a = String::from("falcon");
    let b = "owl";

    println!("{}", longest(&a, b));
}

The longest function returns the longer of two string slices.

fn longest(x: &str, y: &str) -> &str {

The returned reference is borrowed either from x or from y, and which one is decided at run time. The compiler checks each function on its own, using only the signature, so it cannot tell how long the result stays valid.

$ cargo run -q
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.

Lifetime annotations

We add a lifetime parameter, as the compiler suggests.

main.rs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let a = String::from("falcon");
    let b = "owl";
    println!("{}", longest(&a, b));

    let outer = String::from("a long string");
    {
        let inner = String::from("short");
        let res = longest(&outer, &inner);
        println!("{res}");
    }
}

Lifetime parameters are declared in angle brackets, like generic type parameters.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

The signature says: for some lifetime 'a, both parameters live at least as long as 'a, and the returned reference is valid for 'a as well. In practice, 'a becomes the shorter of the two input lifetimes.

let outer = String::from("a long string");
{
    let inner = String::from("short");
    let res = longest(&outer, &inner);
    println!("{res}");
}

Here 'a is the lifetime of inner. The result is used inside the inner block, where both strings are alive, so the program compiles.

$ cargo run -q
falcon
a long string

The shorter lifetime wins

The annotation lets the compiler check every call of the function.

main.rs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let outer = String::from("a long string");
    let res;

    {
        let inner = String::from("short");
        res = longest(&outer, &inner);
    }

    println!("{res}");
}

This time the result is stored in a variable of the outer scope.

{
    let inner = String::from("short");
    res = longest(&outer, &inner);
}

println!("{res}");

At run time res would point to outer, which is still alive. The compiler does not reason about values, however. According to the signature, the result may borrow from inner, which is dropped at the end of the block. The program is rejected.

$ cargo run -q
error[E0597]: `inner` does not live long enough
  --> src/main.rs:11:31
   |
10 |         let inner = String::from("short");
   |             ----- binding `inner` declared here
11 |         res = longest(&outer, &inner);
   |                               ^^^^^^ borrowed value does not live long enough
12 |     }
   |     - `inner` dropped here while still borrowed
13 |
14 |     println!("{res}");
   |                --- borrow later used here

For more information about this error, try `rustc --explain E0597`.

Unrelated parameters

Only parameters that the result can borrow from need to share its lifetime.

main.rs
fn first_field<'a>(line: &'a str, sep: &str) -> &'a str {
    line.split(sep).next().unwrap_or("")
}

fn main() {
    let line = String::from("John Doe;gardener;34");
    let name;

    {
        let sep = String::from(";");
        name = first_field(&line, &sep);
    }

    println!("{name}");
}

The first_field function returns the first field of a line split by a separator.

fn first_field<'a>(line: &'a str, sep: &str) -> &'a str {

The result is a part of line, never of sep. The signature ties the result only to line; sep gets its own independent lifetime.

{
    let sep = String::from(";");
    name = first_field(&line, &sep);
}

println!("{name}");

Thanks to the precise signature, the separator may be dropped while the result is still in use. With 'a on both parameters, this code would fail like the previous example.

$ cargo run -q
John Doe

Lifetime elision

Many functions take and return references without any annotations. The compiler fills in the lifetimes using three elision rules.

main.rs
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

fn count_char(s: &str, c: char) -> usize {
    s.chars().filter(|&x| x == c).count()
}

struct Document {
    title: String,
    body: String,
}

impl Document {
    fn title(&self) -> &str {
        &self.title
    }

    fn find(&self, word: &str) -> Option<&str> {
        self.body.split_whitespace().find(|w| w.contains(word))
    }
}

fn main() {
    let text = "an old falcon";
    println!("{}", first_word(text));
    println!("{}", count_char(text, 'o'));

    let doc = Document {
        title: String::from("Notes"),
        body: String::from("The falcon is a bird of prey"),
    };
    println!("{}", doc.title());
    println!("{:?}", doc.find("pre"));
}

None of the functions and methods has lifetime annotations.

  1. Each reference parameter gets its own lifetime parameter.
  2. If there is exactly one input lifetime, it is assigned to all output references.
  3. If one of the parameters is &self or &mut self, its lifetime is assigned to all output references.
fn first_word(s: &str) -> &str {

By rules 1 and 2, this signature is the same as the explicit version:

fn first_word<'a>(s: &'a str) -> &'a str {

Writing the lifetimes out is allowed but unnecessary; Clippy warns about it with the needless_lifetimes lint.

fn count_char(s: &str, c: char) -> usize {

A function that returns no reference never needs lifetime annotations.

fn find(&self, word: &str) -> Option<&str> {

The method has two reference parameters, so rule 2 does not apply. Rule 3 does: the result borrows from self, which is correct because the found word is a part of the document's body. The longest function needed annotations because none of the rules applied to it.

$ cargo run -q
an
2
Notes
Some("prey")

Structs holding references

A struct that stores a reference needs a lifetime parameter.

main.rs
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        let part = text.split('.').next().unwrap_or(text);
        Excerpt { part }
    }

    fn words(&self) -> usize {
        self.part.split_whitespace().count()
    }

    fn part(&self) -> &'a str {
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago, never mind how long.");

    let first;
    {
        let ex = Excerpt::new(&novel);
        println!("excerpt: {}", ex.part);
        println!("words: {}", ex.words());
        first = ex.part();
    }

    println!("still valid: {first}");
}

The Excerpt struct keeps a part of a longer text without copying it.

struct Excerpt<'a> {
    part: &'a str,
}

The annotation means that an Excerpt cannot outlive the string its part field refers to.

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {

The lifetime is declared after impl and used in the type name. The constructor ties the excerpt to the text it is created from.

fn words(&self) -> usize {
    self.part.split_whitespace().count()
}

fn part(&self) -> &'a str {
    self.part
}

The part method returns &'a str instead of relying on elision. By rule 3, an elided return type would be tied to &self, the borrow of the excerpt. With 'a, the result is tied to the original text.

let first;
{
    let ex = Excerpt::new(&novel);
    ...
    first = ex.part();
}

println!("still valid: {first}");

Therefore the slice remains usable after the excerpt itself is gone. It borrows novel, which is still alive.

$ cargo run -q
excerpt: Call me Ishmael
words: 3
still valid: Call me Ishmael

Two lifetime parameters

Sometimes a struct holds references with different lifetimes.

main.rs
struct Search<'t, 'k> {
    text: &'t str,
    keyword: &'k str,
}

impl<'t> Search<'t, '_> {
    fn find(&self) -> Option<&'t str> {
        self.text
            .split_whitespace()
            .find(|w| w.starts_with(self.keyword))
    }
}

fn main() {
    let text = String::from("the falcon flew over the field");
    let found;

    {
        let keyword = String::from("fl");
        let s = Search { text: &text, keyword: &keyword };
        found = s.find();
    }

    println!("{found:?}");
}

The Search struct looks for a word in a text that starts with a keyword.

struct Search<'t, 'k> {
    text: &'t str,
    keyword: &'k str,
}

The text and the keyword get separate lifetimes. The found word is a part of the text; the keyword is needed only while searching.

impl<'t> Search<'t, '_> {
    fn find(&self) -> Option<&'t str> {

The result is tied to 't only. The '_ placeholder says that the keyword lifetime is not referred to in the block.

{
    let keyword = String::from("fl");
    let s = Search { text: &text, keyword: &keyword };
    found = s.find();
}

println!("{found:?}");

The keyword is dropped before the result is printed. With a single lifetime for both fields, the result would be limited by the short-lived keyword and the compiler would report that keyword does not live long enough.

$ cargo run -q
Some("flew")

The 'static lifetime

The 'static lifetime denotes data that can live for the entire duration of the program.

main.rs
use std::fmt::Display;

static GREETING: &str = "Hello";

fn level_name(level: u8) -> &'static str {
    match level {
        0 => "debug",
        1 => "info",
        2 => "warning",
        _ => "error",
    }
}

fn show_static<T: Display + 'static>(val: T) {
    println!("{val}");
}

fn main() {
    let s: &'static str = "a string literal";
    println!("{s}");
    println!("{GREETING}");

    let name = level_name(2);
    println!("{name}");

    let owned = String::from("owned String");
    show_static(owned);
    show_static(42);
    show_static("literal");
}

String literals are stored in the program binary and have the type &'static str.

fn level_name(level: u8) -> &'static str {
    match level {
        0 => "debug",
        ...
    }
}

A function can return a reference to a literal without borrowing from any parameter. Since there is no input reference, elision cannot supply the lifetime and we must write it. The 'static lifetime says that the result does not borrow from anything.

fn show_static<T: Display + 'static>(val: T) {

A T: 'static bound means something different: the type T contains no references that could expire. Owned types such as String and i32 satisfy it, even though a particular String value is dropped at some point. Such bounds appear, for example, in std::thread::spawn, because a thread may run for any length of time.

$ cargo run -q
a string literal
Hello
warning
owned String
42
literal

'static bounds and local references

A reference to a local variable does not satisfy a 'static bound.

main.rs
use std::fmt::Display;

fn show_static<T: Display + 'static>(val: T) {
    println!("{val}");
}

fn main() {
    let owned = String::from("owned String");
    show_static(&owned);
}

The &owned reference is valid only until the end of main, not for the whole program.

$ cargo run -q
error[E0597]: `owned` does not live long enough
  --> src/main.rs:9:17
   |
 8 |     let owned = String::from("owned String");
   |         ----- binding `owned` declared here
 9 |     show_static(&owned);
   |     ------------^^^^^^-
   |     |           |
   |     |           borrowed value does not live long enough
   |     argument requires that `owned` is borrowed for `'static`
10 | }
   | - `owned` dropped here while still borrowed
   |
note: requirement that the value outlives `'static` introduced here
  --> src/main.rs:3:29
   |
 3 | fn show_static<T: Display + 'static>(val: T) {
   |                             ^^^^^^^

For more information about this error, try `rustc --explain E0597`.

Lifetimes of iterator items

A common use of lifetimes is an iterator that yields slices of its input.

main.rs
struct Tokenizer<'a> {
    input: &'a str,
}

impl<'a> Iterator for Tokenizer<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<&'a str> {
        let s = self.input.trim_start();
        if s.is_empty() {
            return None;
        }
        let end = if s.starts_with(|c: char| c.is_ascii_digit()) {
            s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len())
        } else {
            s.chars().next().map_or(0, char::len_utf8)
        };
        let (tok, rest) = s.split_at(end);
        self.input = rest;
        Some(tok)
    }
}

fn main() {
    let expr = String::from("12 + 345*(6 - 78)");

    let tokens: Vec<&str> = {
        let t = Tokenizer { input: &expr };
        t.collect()
    };

    println!("{tokens:?}");
}

The Tokenizer splits an arithmetic expression into numbers and single-character tokens.

impl<'a> Iterator for Tokenizer<'a> {
    type Item = &'a str;

The items borrow from the input string, not from the tokenizer. The next method takes &mut self, but its return type uses 'a, so the tokens are independent of that temporary borrow.

let (tok, rest) = s.split_at(end);
self.input = rest;
Some(tok)

The split_at method splits the slice into the token and the remaining input. No string data is copied.

let tokens: Vec<&str> = {
    let t = Tokenizer { input: &expr };
    t.collect()
};

The tokenizer is dropped at the end of the block, yet the collected tokens stay valid, because they borrow expr.

$ cargo run -q
["12", "+", "345", "*", "(", "6", "-", "78", ")"]

The anonymous lifetime

The '_ placeholder stands for a lifetime that we do not need to name.

main.rs
use std::fmt;

struct Excerpt<'a> {
    part: &'a str,
}

impl fmt::Display for Excerpt<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "\"{}...\"", self.part)
    }
}

fn evens(vals: &[i32]) -> impl Iterator<Item = &i32> {
    vals.iter().filter(|x| *x % 2 == 0)
}

fn excerpt(text: &str) -> Excerpt<'_> {
    Excerpt { part: &text[..text.len().min(12)] }
}

fn main() {
    let vals = vec![1, 2, 3, 4, 5, 6];
    let res: Vec<_> = evens(&vals).collect();
    println!("{res:?}");

    let text = String::from("It was a bright cold day in April");
    println!("{}", excerpt(&text));
}

We implement Display for a struct with a lifetime and write two functions that return borrowing values.

impl fmt::Display for Excerpt<'_> {

The implementation does not refer to the lifetime, so we use '_ instead of declaring impl<'a>.

fn excerpt(text: &str) -> Excerpt<'_> {

In a return type, '_ uses the elided lifetime. Writing just Excerpt compiles as well, but hides the fact that the result borrows from text; the compiler warns about it with the mismatched_lifetime_syntaxes lint.

fn evens(vals: &[i32]) -> impl Iterator<Item = &i32> {

The returned iterator borrows the slice. In the 2024 edition an impl Trait return type automatically captures the lifetimes of the parameters. Older editions required writing impl Iterator<Item = &i32> + '_.

$ cargo run -q
[2, 4, 6]
"It was a bri..."

Non-lexical lifetimes

A borrow lasts until the last use of the reference, not until the end of the enclosing block.

main.rs
fn main() {
    let mut vals = vec![1, 2, 3];

    let first = &vals[0];
    println!("first: {first}");

    vals.push(4);
    println!("{vals:?}");
}

We take a reference to the first element and then push a new element.

let first = &vals[0];
println!("first: {first}");

vals.push(4);

The shared borrow ends after first is printed. The mutable borrow needed by push does not overlap with it, so the program compiles. This behaviour is called non-lexical lifetimes.

$ cargo run -q
first: 1
[1, 2, 3, 4]

Overlapping borrows

If we use the reference after the push, the borrows overlap.

main.rs
fn main() {
    let mut vals = vec![1, 2, 3];

    let first = &vals[0];
    vals.push(4);

    println!("first: {first}");
}

The error protects us from a real bug: push may reallocate the vector's buffer, and first would then point to freed memory.

$ cargo run -q
error[E0502]: cannot borrow `vals` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let first = &vals[0];
  |                  ---- immutable borrow occurs here
5 |     vals.push(4);
  |     ^^^^^^^^^^^^ mutable borrow occurs here
6 |
7 |     println!("first: {first}");
  |                       ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

Lifetimes with generic types

Lifetime parameters can be combined with generic type parameters and trait bounds.

main.rs
use std::fmt::Display;

fn longest_with_note<'a, T>(x: &'a str, y: &'a str, note: T) -> &'a str
where
    T: Display,
{
    println!("note: {note}");
    if x.len() >= y.len() { x } else { y }
}

fn main() {
    let a = String::from("sunrise");
    let b = String::from("dusk");

    let res = longest_with_note(&a, &b, 2026);
    println!("{res}");

    let res = longest_with_note("left", "right", "comparing literals");
    println!("{res}");
}

The function prints a note of any displayable type and returns the longer string.

fn longest_with_note<'a, T>(x: &'a str, y: &'a str, note: T) -> &'a str
where
    T: Display,

Lifetime parameters come first in the angle brackets, followed by type parameters. The note is passed by value and does not take part in the lifetime relationship.

$ cargo run -q
note: 2026
sunrise
note: comparing literals
right

Source

The Rust Programming Language: Validating References with Lifetimes

The Rust Reference: Lifetime elision

In this article we have worked with lifetimes in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.