The compiler won't guess your references
You write a function to find the longer of two string slices. In Python, you return one of the inputs and move on. In Rust, the compiler rejects you with E0106 (missing lifetime specifier). It refuses to guess which input the output depends on. If the first input drops but the second stays alive, and your function returned the first, you'd have a dangling pointer. Rust won't allow that ambiguity. You have to tell the compiler explicitly how the lifetimes connect. That's what lifetime bounds do.
Lifetimes are contracts, not timers
A lifetime is a region of code where a reference is valid. A lifetime bound is a constraint that links the lifetimes of different references together. The syntax 'a looks like a type parameter, because it is. T stands for "some type". 'a stands for "some duration".
When you write fn foo<'a>(x: &'a str), you're introducing a lifetime parameter 'a and saying x borrows data for that duration. The bound appears when you use 'a in multiple places. Writing fn longest<'a>(x: &'a str, y: &'a str) -> &'a str creates a contract: the returned reference must live as long as 'a, and both x and y must also live as long as 'a. The compiler uses this to prove the result cannot outlive the data it points to.
Treat 'a as a variable for time, not magic. It's a placeholder that gets solved to a concrete scope during compilation.
Minimal example: tying inputs to outputs
Here is the standard pattern for a function that returns a reference derived from its inputs.
/// Returns the longer of two string slices.
/// The lifetime 'a ties the output to both inputs.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// The compiler sees 'a on x, y, and the return type.
// It infers that the result lives as long as the shorter of x and y.
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
// longest borrows from s1 and s2.
// result is valid only while both s1 and s2 are alive.
let result = longest(&s1, &s2);
println!("Longest is: {}", result);
}
The bound 'a appears on both inputs and the output. This tells the compiler that the output is tied to the inputs. The compiler doesn't care that x and y might have different actual scopes. It only cares that they both satisfy 'a. The result is constrained by the shortest scope among them.
Write the bound. The compiler needs the proof.
How the compiler checks the contract
The compiler validates lifetime bounds by checking scopes. If you try to use a result after one of the inputs drops, the compiler rejects you.
fn main() {
let s1 = String::from("hello");
let result;
{
let s2 = String::from("hi");
// This call requires result to live as long as s2.
result = longest(&s1, &s2);
} // s2 drops here.
// Error: E0597 (borrowed value does not live long enough).
// s2 is gone, but result points to it.
println!("Result: {}", result);
}
The lifetime bound 'a forces result to share the lifetime of s2. When s2 goes out of scope, result becomes invalid. The compiler catches this at compile time. No runtime crash. No garbage collector pause. The bound ensures memory safety by construction.
If the compiler rejects the scope, the lifetime bound is doing its job.
Realistic example: multiple lifetimes and over-constraining
Using a single lifetime parameter for everything is a common mistake. It over-constrains the function, forcing unrelated references to share the same scope. This makes the API harder to use.
/// Takes two references with different lifetimes.
/// Returns a reference tied only to the first input.
fn first_word<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
// We use y for some logic but don't return it.
// The return type uses 'a, so y's lifetime 'b doesn't constrain the output.
let _check = y.len();
x.split_whitespace().next().unwrap_or(x)
}
fn main() {
let s1 = String::from("hello world");
let result;
{
let s2 = String::from("temporary");
// s2 can drop immediately after this call.
// result only depends on s1.
result = first_word(&s1, &s2);
}
// This works. result is tied to s1, which is still alive.
println!("First word: {}", result);
}
Here, x has lifetime 'a and y has lifetime 'b. The return type uses 'a. The compiler knows the result depends only on x. y can drop whenever it wants. If you used 'a for both, the compiler would force y to live as long as x, even though y isn't returned. That would reject valid code where y is short-lived.
Over-constraining kills flexibility. Use distinct lifetimes when inputs have independent scopes.
Bounds on types: T: 'a
Lifetime bounds also appear on generic types. The syntax T: 'a means "type T must live at least as long as 'a". This matters when T might contain references.
/// A wrapper that requires T to live at least as long as 'a.
/// This prevents storing short-lived references in a long-lived container.
struct SafeWrapper<'a, T: 'a> {
data: T,
}
impl<'a, T: 'a> SafeWrapper<'a, T> {
/// Creates a new wrapper.
fn new(value: T) -> Self {
SafeWrapper { data: value }
}
}
fn main() {
let s = String::from("hello");
// &s is &'static str if s is String? No, &s is &str with scope of s.
// SafeWrapper requires the reference inside T to live as long as 'a.
let wrapper = SafeWrapper::new(&s);
// If s drops, wrapper becomes invalid because of the T: 'a bound.
// The compiler enforces this relationship.
println!("Wrapper holds: {}", wrapper.data);
}
If T is &'b str, the bound T: 'a implies 'b: 'a. The reference inside T must live longer than 'a. This stops you from putting a short-lived reference into a struct that claims to live longer. The bound propagates through the type system.
Convention aside: name lifetimes 'a, 'b unless the code is genuinely complex. Descriptive names like 'input or 'output rarely help and often clutter the signature. The compiler errors will tell you which lifetime is the issue. Stick to 'a and 'b.
Pitfalls and compiler errors
Lifetime bounds introduce a few traps. Knowing them saves debugging time.
Elision rules hide bounds. Rust has lifetime elision rules that let you skip writing 'a in simple cases. The rules are:
- If there is exactly one input reference, the output gets that lifetime.
- If there is
&selfor&mut self, the output gets the lifetime ofself. - Otherwise, the compiler refuses to guess.
If you have two input references and one output, elision fails. You get E0106 (missing lifetime specifier). You must write the bound explicitly.
// Elision works: one input ref.
fn first(x: &str) -> &str { x }
// Elision fails: two input refs.
// Error: E0106 (missing lifetime specifier).
// fn combine(x: &str, y: &str) -> &str { x } // Compiler can't guess.
// Fix: add the bound.
fn combine<'a>(x: &'a str, y: &'a str) -> &'a str { x }
Returning owned data with a lifetime bound. If your function returns an owned type like String, you don't need a lifetime bound on the return type. Adding one confuses the compiler.
// Error: E0106 or warning about unused lifetime.
// The return type is String, not &str. 'a is unused.
// fn make_hello<'a>() -> String { String::from("hello") }
// Correct: no lifetime needed.
fn make_hello() -> String { String::from("hello") }
Dangling references from local data. You can't return a reference to data created inside the function. The data drops at the end of the function.
// Error: E0515 (returns reference to local variable).
// fn bad() -> &str { let s = String::from("hi"); &s }
// Fix: return owned data.
fn good() -> String { String::from("hi") }
Don't force a single lifetime on unrelated references. You'll lock yourself out of valid code.
Decision: when to use lifetime bounds
Use lifetime bounds when your function returns a reference derived from one or more input references. Use owned types like String or Vec when the data needs to outlive the inputs or when you want to avoid lifetime complexity in the API. Rely on lifetime elision when you have exactly one input reference and one output reference; the compiler handles the binding automatically. Use multiple distinct lifetime parameters when inputs have different scopes and the output depends on only a subset of them. Prefer ownership when lifetimes make the API painful to use.
Prefer ownership if the API becomes a lifetime puzzle.