Rust's NLL: Smarter Borrows Based on Use, Not Scope
Non-Lexical Lifetimes (NLL) make Rust's borrow checker smarter. A borrow's lifetime ends after its last use, not at the end of its code block. This allows modifying data after a borrow is finished, even if the reference variable is still in scope.
WHY IT EXISTS Rust's original borrow checker was too rigid. It tied a borrow's lifetime to its lexical scope (the surrounding {} block). This often made the borrow last longer than necessary, causing the compiler to reject perfectly safe code and forcing developers into awkward workarounds.
THE MENTAL MODEL Think of Non-Lexical Lifetimes (NLL) as the borrow checker understanding when you are done with a borrow, not just where it was declared. A borrow is now considered to end immediately after its last use, freeing up the original data for other operations. It shifts the analysis from being scope-based to being usage-based.
HOW IT WORKS Instead of just looking at nested code blocks, the NLL-aware compiler analyzes the program's control-flow graph. It determines the precise points where a reference is created, used, and, critically, last used. The borrow's lifetime is the smallest region in this graph that contains all those points. Once the code path moves past the final use, the borrow is over.
WHEN TO USE IT NLL is the default behavior in modern Rust (since the 2018 edition), so you are always using it. It is most noticeable when you store a borrow in a variable, use it, and then want to re-access the original data within the same block. This pattern is now often permitted where it was previously forbidden.
WHEN NOT TO USE IT You don't choose whether to use NLL; it's the standard. However, it doesn't solve all borrowing problems. The fundamental rules of borrowing (one mutable borrow OR multiple immutable borrows at a time) still apply. Complex conditional logic can still lead to borrow checker errors that NLL cannot resolve on its own.
ONE CANONICAL EXAMPLE Before NLL, this code failed: let mut data = vec![...]; let slice = &mut data[..]; capitalize(slice); data.push('d'); // ERROR!. The borrow on data was considered to last until the end of the function because the variable slice lives that long. With NLL, the compiler sees slice is last used in the capitalize call. The borrow ends there, and the subsequent data.push('d') is correctly allowed.
Read the original → rust-lang.github.io
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.