← thecodex.expert · The Codex Family of Knowledge
Rust

Borrow Checker Deep Dive

Code that felt like it should compile in early Rust often does now — Non-Lexical Lifetimes, stabilized in the 2018 edition and later extended to 2015-edition code too, made the borrow checker significantly smarter without changing the underlying safety rule at all.

Rust 1.95+ NLL: 2018 edition, then 2015 too (1.36) Last verified:
Where this bites you

NLL ends a borrow at its last use — but when a function returns a reference from inside a branch, the borrow has to last as long as the caller's whole result, on every path. The classic "return it if it's cached, otherwise insert it" shape is perfectly safe at runtime, and still refused: the compiler can't prove the early-return borrow is dead on the other path.

Rustconditional_return_bug.rs
use std::collections::HashMap;

fn get_or_insert(map: &mut HashMap<u32, String>, key: u32) -> &String {
    if let Some(v) = map.get(&key) {
        return v;
    }
    map.insert(key, format!("user-{}", key));
    &map[&key]
}
// error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
//   map.get(&key)  -- immutable borrow occurs here
//   return v;      -- returning this value requires that `*map` is borrowed for `'1`
//   map.insert(..) -- mutable borrow occurs here
Canonical Definition

Before NLL, a reference's lifetime lasted until the end of its enclosing lexical scope (its innermost {} block), even if the reference was never used again after an early point — this rejected plenty of code that was actually safe. Non-Lexical Lifetimes (NLL) changed the analysis to track a reference's actual LAST USE, ending its borrow there instead. NLL shipped with the 2018 edition first (Rust 1.31, so new 2018-edition crates got it immediately) and was extended to 2015-edition code in Rust 1.36 a few months later — it isn't an edition-specific feature today, every crate gets it regardless of which edition it's written against. The underlying rule — one mutable reference or many immutable, never both — never changed; NLL just made the compiler’s understanding of when a borrow truly ends more precise.

Before and after Non-Lexical Lifetimes

This pattern used to be a compile error pre-2018, because r1 and r2's borrows lasted (lexically) until the end of the block — even though neither is used again after the println!. NLL fixed exactly this class of false rejection.

Rustnll.rs
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);   // r1, r2's LAST USE is right here

let r3 = &mut s;   // fine since NLL (2018+): r1, r2's borrows already ended
r3.push_str(" world");

A borrow that still fails, and why it should

NLL made the checker smarter, not weaker — genuinely conflicting borrows are still rejected, exactly as the safety guarantee requires.

Ruststill_fails.rs
let mut s = String::from("hello");
let r1 = &s;
let r3 = &mut s;   // COMPILE ERROR: r1 is still used below, so its borrow is still active
println!("{}", r1);

This is one of the few places where the checker rejects code that is actually sound (the page's NLL discussion is about exactly this kind of false rejection, and this shape is a known remaining one). The fix isn't to fight it with clones — it's to use an API that does the lookup-then-insert as one operation, so there is only ever one borrow:

Rustconditional_return_fix.rs
use std::collections::HashMap;

fn get_or_insert(map: &mut HashMap<u32, String>, key: u32) -> &String {
    map.entry(key).or_insert_with(|| format!("user-{}", key))
}

let mut cache = HashMap::new();
println!("{}", get_or_insert(&mut cache, 7));   // user-7
println!("{}", get_or_insert(&mut cache, 7));   // user-7  (found, not re-inserted)

Reading a borrow-checker error message

Rust's compiler errors for borrow violations are unusually detailed: they name which borrow conflicts with which, point at both locations in the source, and often suggest the exact fix (adding a clone, reordering statements, or restructuring with a block to shorten a borrow's lifespan). Learning to read these messages closely, rather than treating them as an obstacle, is one of the fastest ways to build an accurate mental model of ownership.

Sources

1
The Rust Project. The Rust Reference, and the "Non-Lexical Lifetimes RFC", rust-lang.github.io/rfcs.