Skip to content
← Back to explainers

Rust explainer

'static: references and type bounds

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

The difference between a &'static T reference and a T: 'static bound: why owned values such as String satisfy the bound, why moving a container of borrows does not, and how trait objects get a default lifetime.

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

'static appears in two related but different positions, and mixing them up leads to a common misreading. For example, thread::spawn requires its closure type F to satisfy F: Send + 'static. That bound does not mean a thread can only use string literals or data that lives until the program exits.

This article separates the two uses:

  • a lifetime on a reference, as in &'static str, and
  • an outlives bound on a type, as in T: 'static.

A lifetime is the region of the program during which a borrow must stay valid. 'static is the longest one: the rest of the program's execution.

Meaning one: &'static T is a reference

let s: &'static str = "hello";

This is a reference whose referent (the data it points to) stays valid for the rest of the program. The reference itself is an ordinary local variable; only the data it points to has to remain valid that long. Common sources:

  • String literals. "hello" has type &'static str. The text is stored as part of the compiled program, not on the stack or heap at run time.
  • static items. A static has one storage location that lasts for the whole program, so &NAME can be a &'static reference.
  • Box::leak. It turns a heap allocation into a reference that is never freed automatically. See the last section.

So &'static T describes how long the referenced data is guaranteed to stay valid.

Meaning two: T: 'static is a bound

fn needs_static<T: 'static>(_t: T) {}

This is a constraint on a type, not an instruction to keep a value alive. T: 'static reads "T outlives 'static": the type must not contain any borrowed data that could become invalid before the program ends. Types with no references or lifetime parameters, such as String, Vec<u8> and i64, satisfy it. A value of such a type can still be dropped at the end of an ordinary local scope:

fn needs_static<T: 'static>(_t: T) {}

fn main() {
    let s = String::from("noon");
    needs_static(s); // compiles; `s` is moved in and dropped when the call ends
}

A String does point at heap memory, but it owns that memory. The bound is about borrowed data, so owning a heap buffer does not stop a type from being 'static.

Owning the outer container is not enough, though, because a container can hold references to someone else's data. This example does not compile:

fn needs_static<T: 'static>(_t: T) {}

fn main() {
    let source = String::from("local");
    let owned = vec![source.as_str()];
    needs_static(owned); // error[E0597]: `source` does not live long enough
}

owned has type Vec<&str> where the &str borrows source. The vector owns its own buffer, but its elements are borrows. For Vec<&'x str> to satisfy 'static, the borrow lifetime 'x would have to be 'static, and source is dropped at the end of main. Moving the vector does not extend that borrow. Two variations do satisfy the bound:

  • Vec<String> owns its strings, so it contains no borrows.
  • Vec<&'static str>, for example vec!["a", "b"], contains references, but they are already valid for the rest of the program.

In short: T: 'static does not mean "this value lives forever". It means "this type does not depend on any borrow shorter than 'static".

Check your understanding thread::spawn requires F: Send + 'static, where F is the closure's type. What does the 'static part rule out, and what does it still allow? Show answer

It rules out closures that capture references to data that might be freed while the thread is still running, such as a reference to a local variable of the function that called spawn. It still allows closures that capture owned values, such as a String, even though those values are dropped when the thread finishes. The separate Send bound must also hold: every captured value has to be safe to move to another thread.

let s = String::from("hi");
let h = std::thread::spawn(move || println!("{s}")); // compiles
h.join().unwrap();

move makes the closure take ownership of s. The closure's type now contains a String, which satisfies 'static, so the bound holds. The string is dropped when the closure is dropped after the thread finishes.

Moving a container of borrows into the closure does not help. Capturing vec![s.as_str()] with move still fails with E0597, for the same reason as the needs_static example above.

Why the bound exists: a thread created by thread::spawn can keep running after the function that spawned it returns, because nothing requires you to call join. If the closure held a &str into one of that function's locals, the local could be freed while the thread still reads it. F: 'static rules out such references.

When you do want to borrow local data from threads, use std::thread::scope. Every thread spawned inside the scope is joined before scope returns, so the compiler allows those threads to borrow data that outlives the scope call. The 'static requirement belongs to thread::spawn, not to threads in general.

Reference types in const and static items

In the type of a const or static item, an elided reference lifetime means 'static:

const GREETING: &str = "hi"; // type: &'static str
static NAME: &str = "world"; // type: &'static str

Writing 'static explicitly is still valid Rust, and rustc accepts it. Clippy reports it with the redundant_static_lifetimes lint, which is in the warn-by-default clippy::style group:

const G: &'static str = "hi";
error: constants have by default a `'static` lifetime
 --> src/main.rs:1:11
  |
1 | const G: &'static str = "hi";
  |          -^^^^^^^---- help: consider removing `'static`: `&str`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_static_lifetimes
  = note: `-D clippy::redundant-static-lifetimes` implied by `-D warnings`
  = help: to override `-D warnings` add `#[allow(clippy::redundant_static_lifetimes)]`

It appears as an error here because this site runs Clippy with -D warnings, which turns every enabled warning into an error. For a static item the message says "statics have by default a 'static lifetime". Write &str in these item types.

Trait objects have a default lifetime too

A trait object such as dyn Fn() -> usize can capture borrowed data, so it also has a lifetime bound, written dyn Fn() -> usize + 'a. When you leave the bound out, Rust picks a default. Inside Box in a type alias, the default is 'static:

type Handler = Box<dyn Fn() -> usize>;
// means: Box<dyn Fn() -> usize + 'static>

So a Handler cannot hold a closure that borrows a function argument. The same 'static default applies to Box<dyn Fn() -> usize> written in a function signature. A function that returns a closure borrowing its argument must connect the object's lifetime to that argument:

fn counter<'a>(text: &'a str) -> Box<dyn Fn() -> usize + 'a> {
    Box::new(move || text.len())
}

If you remove + 'a, the compiler reports "lifetime may not live long enough": the returned closure borrows text, so it cannot meet the default 'static bound for every possible input. Writing + '_ instead of + 'a also compiles. '_ asks for the usual lifetime-elision rules, which here pick the lifetime of the single reference argument. It does not turn off lifetime checking.

Inside a function body, a type annotation on a let binding is treated differently: the object's lifetime is inferred from how the value is used. This compiles even though the closure borrows the local text:

let text = String::from("hi");
let count: Box<dyn Fn() -> usize> = Box::new(|| text.len());
assert_eq!(count(), 2);

So do not read every Box<dyn Trait> as 'static. The default depends on the containing type, any lifetime bounds on the trait, and whether the type is written in a signature or item or inside a function body. The full rules are in the Rust Reference under default trait-object lifetimes.

Check your understanding Consider each numbered call on its own, as if the other three were not there. Which compile? For each failure, say which of the two meanings is involved. Show answer
fn a<T: 'static>(_t: T) {}
fn b(_s: &'static str) {}

fn main() {
    let owned = String::from("x");
    let literal = "y";
    let borrowed: &str = &owned;

    a(owned.clone());   // 1
    a(borrowed);        // 2
    b(literal);         // 3
    b(borrowed);        // 4
}

1 compiles. owned.clone() is a String, which contains no borrows, so String: 'static holds. This is the bound meaning.

2 fails. borrowed has type &'x str, where 'x is the lifetime of the borrow &owned. The bound &'x str: 'static requires 'x to be 'static, but owned is dropped at the end of main. The compiler reports E0597, "owned does not live long enough". This is the bound meaning: the type contains a short-lived reference.

3 compiles. literal is a string literal, so its type really is &'static str. This is the reference meaning.

4 fails with the same E0597 error. This time the requirement comes from the reference meaning: b's parameter type is &'static str, and a borrow of owned cannot last that long.

Calls 2 and 4 fail for the same underlying reason, a short-lived borrow used where a 'static one is required, but they reach it differently. Call 2 goes through a bound on a generic type; call 4 goes through the lifetime written on a reference type. If you compile the whole program at once, rustc reports a single E0597 at &owned, because both calls make the same borrow need to last for 'static; deleting either call still leaves that error.

&'static mut T and Box::leak

&'static mut T is also a valid type. Like any &mut, it is an exclusive reference: while it is in use, nothing else can read or write the value. One safe way to get one is Box::leak:

let r: &'static mut String = Box::leak(Box::new(String::from("hi")));
r.push('!');
assert_eq!(r, "hi!");

Box::leak gives up automatic cleanup: the allocation is not freed when r goes out of scope. That is acceptable for a value created once and used until the program exits, such as configuration loaded at startup. Calling it repeatedly, for example once per request, leaks memory each time.

Summary

you see it means
&'static T a reference whose data stays valid for the rest of the program
T: 'static the type contains no borrow shorter than 'static; a value of the type can still be dropped at any point
const X: &str or static X: &str implicitly &'static str; Clippy flags an explicit 'static
Box<dyn Fn()> in a type alias or function signature defaults to Box<dyn Fn() + 'static>
Box<dyn Fn()> in a let annotation inside a function body the object lifetime is inferred from use
std::thread::spawn the closure must be Send + 'static; use std::thread::scope to borrow locals

step 16 of 22

Ownership III: Lifetimes, Explicitly

Up next Store owned and borrowed closures in registries