The Rust Engineering Handbook / Chapter 9
Reborrowing, Non-Lexical Lifetimes, Two-Phase Borrows, and Higher-Ranked Bounds
Explain advanced borrow patterns, their limits, and callback or lending APIs that work over short-lived references.
Who chooses the short borrow?
Chapter 8 gave names to relationships among borrowed inputs and outputs. More demanding APIs add another question: who chooses each short relationship?
A helper called through &mut Entry usually needs the entry only for that call. The outer function owns the longer mutable capability and lends it inward. A callback invoked once per account must be willing to accept a new relationship chosen by the scanning function on every invocation. A cursor that returns a view makes a different promise: the caller may keep that view, but the cursor cannot advance while the view is still in use.
These are not variations of “the borrow checker got smarter.” Reborrowing explains how an existing capability is lent. Non-lexical lifetime analysis determines how far a borrow’s uses require it. Two-phase borrowing admits a narrow class of implicit mutable-receiver calls whose arguments also inspect the receiver. A higher-ranked bound says that a callback works for every call relationship rather than one relationship chosen in advance.
None permits conflicting active access. None is a runtime lock, reference-count operation, or destructor event. The useful question throughout this chapter is concrete: who chooses the borrow, what can escape through it, and which operation must wait until its last use?
Reborrowing preserves the outer reference for later use
This function calls an inner helper twice:
fn increment_twice(value: &mut i64) {
fn increment(value: &mut i64) {
*value += 1;
}
increment(&mut *value);
increment(&mut *value);
}
Each &mut *value is a reborrow with a relationship short enough for one call. During that call, the outer reference cannot be used incompatibly. After the call, the reborrow ends and the outer reference can be reborrowed again.
Rust inserts implicit reborrows in many call contexts, so increment(value); increment(value); also works. Writing the explicit form is useful when teaching or diagnosing which reference is moved and which is reborrowed. Mutable references do not implement Copy; context-sensitive reborrowing is not implicit copying.
Returning the nested borrow changes the requirement:
fn field<'short>(entry: &'short mut Entry) -> &'short mut i64 {
&mut entry.cents
}
The caller may use the returned field borrow, and the original entry remains unavailable for conflicting access until that returned relationship’s last use. “The helper returned” is not enough to end a borrow that escaped through its output.
Figure 9-1 keeps the mechanisms in separate lanes. Read reservation and activation independently from last use, and read the callback lane as “for every call relationship,” not as one long borrow.
Last use, not the closing brace
Before non-lexical lifetime analysis, accepted code tracked borrows more closely to lexical scopes. Current Rust reasons from control flow and uses:
let account = entry.account.as_str();
println!("{account}");
entry.account.push_str("-closed");
The shared borrow of entry.account is needed through the print. Mutating that same String afterward is accepted because the reference is not used again. If the print moved below push_str, the operations would conflict. In a branch, the required region follows paths on which the reference remains live. A returned, stored, or captured reference can extend the relationship much further than the line that created it.
NLL is compile-time analysis. It does not inject unlocks, release reference counts, or run destructors at last use. Destruction follows drop-scope rules and explicit early drop behavior, covered in Chapter 11.
When a borrow seems unexpectedly long, search for the last dependent use, including:
- a later formatting or logging call;
- a value returned from the function;
- storage in a struct, enum, or collection;
- capture by a closure or async state machine;
- a match result that carries a reference across arms;
- an error value containing borrowed context.
Two-phase borrows solve a narrow receiver-order problem
This familiar call compiles:
let mut values = vec![10, 20];
values.push(values.len());
Method-call evaluation needs a mutable receiver for push, while the argument calls len through shared access. Two-phase borrowing permits an eligible implicit mutable receiver borrow to be reserved, lets argument evaluation perform compatible access, then activates exclusivity for the call.
This is not a general permission to overlap &mut and &:
let receiver = &mut values;
let length = values.len(); // rejected: explicit mutable borrow already conflicts
receiver.push(length);
Eligibility and compiler behavior are deliberately narrower than “mutable borrows start whenever convenient.” Treat two-phase borrowing as support for specific implicit autoref patterns, not an API design tool to rely on for complex overlapping access.
If argument evaluation mutates the same object, invokes reentrant callbacks, or produces obscure ordering, split it into named steps. Readability and stable invariants matter more than compressing a call.
The scanner chooses each callback borrow
This API visits account names without letting the visitor demand one particular long-lived reference:
fn visit_accounts<F>(entries: &[Entry], mut visitor: F)
where
F: for<'a> FnMut(&'a str),
{
for entry in entries {
visitor(&entry.account);
}
}
Read for<'a> as “for every valid call lifetime 'a.” visit_accounts, not the caller, chooses a fresh short borrow on each iteration. The callback cannot require that every account arrive with one preselected external lifetime.
This is universal quantification, not a request for 'static. It makes the callback more general and prevents retention through ordinary safe typing. A closure may count names or copy selected names into owned storage. It cannot assign the borrowed parameter to an outside Option<&str>: that parameter is allowed to be valid only for the current invocation.
HRTBs appear in bounds such as for<'a> Fn(&'a T) and trait relationships involving references. Use them when the provider chooses fresh borrows per call. Do not add them to appear advanced; an ordinary named lifetime is correct when the caller supplies one specific relationship that the result may retain.
A diagnostic can reveal the wrong lifetime owner
Compare:
fn inspect<'a, F>(value: &'a str, f: F)
where
F: Fn(&'a str),
with:
fn inspect<F>(value: &str, f: F)
where
F: for<'a> Fn(&'a str),
The first lets the caller supply one particular 'a tied to value. That is appropriate when the callback contract genuinely uses that caller-chosen relationship. The second requires a callback that works for any short borrow chosen inside inspect. That stronger promise is appropriate when inspect controls invocation and forbids borrowed input from escaping. Neither signature is a decorative way to satisfy the compiler; each assigns authority over the relationship to a different party.
When diagnostics say borrowed data escapes a closure or one type is “not general enough,” write down who chooses the lifetime, whether the callback may store the input, and whether output contains a borrow. That quantifier audit is more reliable than adding move or 'static.
Returning the view transfers control to the caller
The standard Iterator trait’s Item type is chosen once for an implementation. Some APIs need each yielded item to borrow from the current mutable borrow of the iterator-like object. A generic associated type can express a dedicated lending trait on stable Rust:
trait LendingCursor {
type Item<'a>
where
Self: 'a;
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
An implementation can set Item<'a> = &'a str. Unlike the visitor, this interface deliberately lets a borrow escape the method call. The output remains tied to that call’s mutable borrow, so callers cannot call next again while still using the prior yielded reference. That restriction can protect a reusable buffer or cursor state: advancing may overwrite exactly the storage to which the previous item refers.
That waiting rule, not surface resemblance to Iterator, should decide whether lending is honest. Alternatives include:
- yield owned items, paying transfer or allocation costs;
- yield indexes or handles, moving validity checks to a repository;
- borrow from an immutable backing collection with an ordinary iterator;
- process items through an HRTB callback, preventing retention;
- use a bespoke cursor API with explicit operations.
Do not claim that a lending trait is “an iterator but faster.” It has different composability, retention, and ergonomics. Measure copying and state reuse, and account for the smaller ecosystem surface.
Keep the short relationship inside its natural boundary
Reborrowing keeps a synchronous mutation domain narrow without allocating. A higher-ranked callback can observe a sequence without allocating each item, but it gives up borrowed retention and may expose difficult diagnostics to API consumers. A lending cursor restores retention one item at a time, then makes progress wait for the item borrow. Owned commands pay copying or transfer costs to cross threads, queues, and long-lived tasks without retaining the original owner.
The cheapest local signature is not automatically the cheapest system boundary. Do not hold a returned borrow across a callback that may reenter the owner, or lend a view into a buffer that another operation may refill. In async code, a borrow crossing .await becomes part of the future’s stored state; that deserves explicit lifecycle and Send analysis in Chapter 59.
Failure modes
- Treating a mutable reference as copied when a call actually reborrows it.
- Assuming NLL runs cleanup at last use.
- Using two-phase borrowing as justification for arbitrary alias overlap.
- Adding
'staticwhen the real need is a callback valid for any short borrow. - Returning a reference from a callback API intended to prevent retention.
- Designing a lending cursor without documenting that the next call waits for the prior item borrow to end.
- Compressing evaluation into a clever call that obscures reservation, activation, or reentrancy.
Senior review checklist
- Is a mutable reference moved, reborrowed, returned, stored, or captured?
- What exact use ends each nested borrow?
- Does any output or closure extend the relationship beyond the helper call?
- Is a two-phase borrow actually an eligible implicit receiver pattern?
- Who chooses each lifetime: caller, callee, or each callback invocation?
- Does
for<'a>express a real non-retention promise? - Would an owned item or handle make the boundary easier to operate?
- Are callbacks, reentrancy, async suspension, and buffer reuse addressed?
Engineering exercise: change who controls progress
Begin with an account scanner accepting F: for<'a> FnMut(&'a str). Demonstrate successful counting and collection into owned Strings. Before compiling it, write the closure that tries to store one borrowed input in an outside Option<&str> and predict which relationship cannot be proved; preserve the compiler rejection as a fixture.
Then expose the same entries through a lending cursor. Hold the first returned item while attempting a second next call, predict the conflict, and preserve that rejection too. Finally choose an owned-item design, the visitor, or the lending cursor for a scanner that sometimes queues matches for asynchronous processing. Defend the choice in terms of who controls progress, what may be retained, allocation, diagnostics, and buffer reuse—not merely whether the code compiles.
Durable takeaways
- Reborrowing lends capability from an existing reference for a shorter relationship; it does not copy the reference’s exclusive power.
- NLL follows control-flow uses and changes compile-time acceptance, not runtime cleanup.
- Two-phase borrowing is a narrow facility for eligible implicit mutable receiver patterns.
for<'a>lets a provider call a callback with fresh short borrows and expresses a strong non-retention boundary.- Lending APIs tie each output to the current borrow of the lender and trade ordinary iterator composability for tighter reuse contracts.
These mechanisms stretch compile-time relationships without weakening them. When a sound API genuinely requires mutation through shared access, the design must make a different move: choose an explicit runtime enforcement boundary and accept its failure or synchronization costs.
Sources and version notes
- Rust Reference: higher-ranked trait bounds
- Rust Compiler Development Guide: non-lexical lifetimes
- Rust Compiler Development Guide: two-phase borrows
- Rust Reference: generic associated types
- Verified with Rust 1.97.0 and Rust 1.85.0. Two-phase-borrow details are described as compiler acceptance behavior, not a runtime mechanism or general aliasing relaxation.
Continue reading
Full table of contents