Before the 2018 edition, 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. 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.
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.
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);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.