Skip to content
← All Rust articles

Rust explainer

A lifetime is a constraint, not a duration

About 4 min to read. Allow extra time to work through examples.

Learn how lifetime annotations relate borrowed inputs and outputs, why a borrow can end before its variable leaves scope, and how to keep returned references valid.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A lifetime annotation describes how long a reference must remain valid in relation to other references. It does not keep the referenced value alive. If the data would be destroyed too soon, the compiler rejects the program; adding an annotation cannot postpone that destruction.

Describe the relationship between inputs and output

Consider a function that returns one of two borrowed strings:

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

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

Both inputs must be valid for the lifetime used by the result. They do not need to have been created together or be destroyed together. Here the caller uses the result while both strings are alive, even though outer outlives inner. The function compares byte lengths and chooses a on a tie.

The shared 'a allows the function to return either input. At a call site, the compiler checks that the inputs can support all required uses of the result. It need not choose the largest possible lifetime or make the two owned strings live equally long.

If you instead store the result outside the inner block and use it after inner is dropped, the call cannot satisfy that contract. This remains true even when you know these particular contents make outer win. The caller is checked against the function's signature, which allows either input to be returned.

Read an outlives bound

The bound 'a: 'b means that 'a outlives 'b. For shared references, a reference valid for 'a can be used for the shorter requirement 'b:

fn shorten<'a: 'b, 'b>(text: &'a str) -> &'b str {
    text
}

fn main() {
    let owned = String::from("hello");
    assert_eq!(shorten(&owned), "hello");
}

This does not shorten the life of owned. It limits what the returned reference promises. The reverse conversion is not generally possible: a reference valid for a shorter borrow cannot promise validity beyond it.

A useful model is a region of program points where a reference must be valid. In that model, an outlives relationship is containment: the longer region includes the shorter one. This helps explain reference lifetimes, but it is not a complete recipe for predicting every borrow-checker decision. Type relationships, aliasing restrictions and control flow also matter.

Shared references are covariant in their lifetime, which permits this shortening. Do not extend that statement to arbitrary transformations of nested types or mutable reference contents; those have their own variance rules.

A borrow can end before its variable leaves scope

fn main() {
    let mut values = vec![1, 2, 3];
    let first = &values[0];
    assert_eq!(*first, 1);

    values.push(4);
    assert_eq!(values, [1, 2, 3, 4]);
}

first is still in lexical scope at the push, but it is no longer used. Non-lexical lifetimes let the compiler end this shared borrow before the mutation. The vector still owns its elements throughout; ending a borrow does not destroy the borrowed data.

Add another use of first after push and the program is rejected. The shared borrow would then overlap the mutation, which could reallocate the vector's storage. Last use explains this example, but it is not a universal rule that every borrow ends at the last visibly written reference access: destructors and references stored in other values can also require validity.

Returning a reference requires a valid source

With one borrowed input, lifetime elision can express the relationship for you:

fn first_line(text: &str) -> &str {
    text.lines().next().unwrap_or("")
}

fn main() {
    let text = String::from("first\nsecond");
    assert_eq!(first_line(&text), "first");

    let saved = {
        let temporary = String::from("keep this\nnext");
        first_line(&temporary).to_owned()
    };
    assert_eq!(saved, "keep this");
}

The first result borrows from text. The second call copies the selected line into an owned String, so saved remains usable after temporary is dropped. Another option is to keep the source string alive for as long as its borrowed result is needed. Choose based on who should own the data.

Changing first_line to return &'static str would not extend its input's lifetime. Its body can return a slice of an arbitrary caller-owned string, so that stronger promise is rejected. Returning a string literal can satisfy a static-reference contract, but it would be a different operation.

What the compiler checks

Lifetime parameters do not add runtime counters or expiration checks to references. Their relationships are checked during compilation. This does not promise byte-identical machine code for every pair of differently annotated functions, or mean all safe Rust abstractions have no runtime checks: RefCell, for example, checks borrowing dynamically.

Successful output tests cannot establish that a borrowed API accepts the intended callers. Compile examples that should be accepted, and check that examples which would outlive their data are rejected. Both are useful when designing a function that returns references.

For the language rules behind these examples, see the Rust Reference on lifetime elision and lifetime bounds.

step 2 of 22

Ownership III: Lifetimes, Explicitly

Up next Borrow it or own it: your first lifetime decision