Non-Lexical Lifetimes
Non-Lexical Lifetimes (NLL) is the rule in modern Rust whereby the lifetime of a reference (&T or &mut T) ends immediately after the last statement where that reference is used, rather than lasting until the closing curly brace } of the lexical block in which the reference variable was declared.
Lexical Lifetimes (Old Pre-2018 Rust) vs Non-Lexical Lifetimes (Modern Rust)
Before NLL was stabilized in the Rust 2018 Edition, a reference's borrow remained locked until the reference variable went out of scope at }, forcing programmers to wrap borrows in artificial { ... } blocks just to release them early:
Old Lexical Rule (Pre-NLL): Modern Non-Lexical Lifetimes (NLL):
fn main() { fn main() {
let mut x = 10; let mut x = 10;
let r = &x; ──┬── let r = &x; ──┬── Borrow starts
println!("{r}"); │ println!("{r}"); ──┴── Borrow ENDS here!
x += 1; // ❌ Rejected │ x += 1; // ✅ Allowed! (`r` is dead)
} ──┴── }
fn main() {
let mut scores = vec![10, 20, 30];
let first: &i32 = &scores[0];
println!("First score: {first}");
// `first` is still in lexical scope until the end of `main`,
// but under NLL its borrow of `scores` ENDS on the line above!
// Therefore, mutating `scores` here compiles cleanly without extra `{ ... }` braces:
scores.push(40);
println!("Updated scores: {scores:?}");
}
First score: 10
Updated scores: [10, 20, 30, 40]
Conditional Last Use Across Branches
NLL tracks reference liveness along every branch of an if/else or match expression independently. If a reference r is used inside the if branch, its borrow ends right after that use inside the if branch—and in the else branch where r is not used at all, the borrow is already finished on entry to the else branch!
fn main() {
let mut data = String::from("hello");
let r: &str = &data;
if r.starts_with('h') {
println!("Read r in if-branch: {r}"); // `r` ends here!
data.push_str(", world!"); // Allowed right after `r`'s last use!
} else {
// `r` is never used in the `else` branch, so `data` can be mutated immediately:
data.clear();
}
println!("Result: {data}");
}
Read r in if-branch: hello
Result: hello, world!
Important Distinction: NLL Ends Reference Borrows, NOT Owner Drop Timing!
Be careful not to confuse when a reference borrow ends (NLL) with when an owned value's Drop destructor runs:
| Kind of Variable | Governed By | When Does It End / Drop? |
|---|---|---|
Reference borrow (&T, &mut T) |
Non-Lexical Lifetimes (NLL) | Immediately after the last use of the reference (has no Drop side effect) |
Owned value (String, File, MutexGuard) |
Lexical Scope (Drop) |
At the closing brace } of its block (unless moved or passed to drop(x) early), because Drop::drop has observable runtime side effects! |
If a variable holds a MutexGuard or a custom struct implementing Drop that internally holds a reference 'a, its borrow 'a must stay alive until the closing brace } (or an explicit drop(guard)) because Drop::drop(&mut self) will run at } and counts as a final use of the variable!