References and Borrowing

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!