Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-613 · Case file with fixtures · Case 585 of 694 · Compiler evidence

A Borrowed Rust Temporary Usually Dies at the End of the Statement

A helper call can hide that a borrow still points into a temporary owner. Give the owner a local binding or return owned data according to the boundary.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
A helper call hid that the returned reference still points into an unnamed temporary whose ordinary destruction scope is the statement.
First discriminating check
Name the owner before borrowing it, or return owned data when the view must cross the owner's enclosing scope.

String::from("ready") creates an owned temporary. Borrowing it through identity does not transfer that ownership into the returned &str. The failing fixture saves the view for a later statement, so rustc reports E0716.

Expand the temporary into an imaginary local

I debug this error by rewriting the expression mentally. Create a local String, pass a reference to it, return the slice, then drop the local at the statement boundary. The later print would use the slice after its bytes were freed.

The official E0716 explanation uses this expansion and explains Rust's temporary lifetime extension cases. The helper function can make the owner less visible, but it does not change the data flow.

A reference copies an address-like view; it does not keep an allocation alive.

The repair names the owner

The repaired fixture binds the String to owner before borrowing it. Locals normally live to their destruction scope, so the owner remains valid through the assertion using selected.

If the view must leave the whole function, a local is still not enough. The function should return String, another owned representation, or a borrow connected to caller-owned input. I choose based on who should pay for and retain storage.

Naming the owner is valuable even where temporary lifetime extension would compile. It makes drop timing reviewable and provides a place for error context or reuse.

Lifetime extension applies only to specific syntax

Rust extends some temporaries when a borrow is stored directly in a let binding or aggregate. Passing the borrow through a function call, as in this case, generally does not receive the same extension.

I do not memorise this as “temporaries always die at semicolon” because the documented exceptions matter. I consult the Reference temporary scopes when behaviour affects an API, and I use a named owner when clarity matters.

Edition changes can adjust temporary scopes for certain expressions. Compile fixtures should pin edition and toolchain, especially around if let, match scrutinees, and tail expressions.

Method chains can hide the same problem

Creating a container, calling a method that borrows from it, and saving the view may fail when the container is temporary. Examples include String slices, path components, locked data, and values returned by accessors.

I inspect the final result type. If it contains a reference, I trace through every chain step to the concrete owner. A method name such as as_str, as_slice, iter, or get often signals borrowing, but the signature is authoritative.

to_string, to_owned, and cloned can create ownership, though each has cost and semantics. I use them only when independent storage is the desired boundary.

Guard temporaries can affect locks and resources

Temporary destruction is not only about dangling references. A lock guard created in an expression may release the lock at a statement boundary or be extended longer than expected. Database guards and RAII handles have similar operational effects.

I bind guards explicitly when lock duration matters, keep critical sections small, and never hold synchronous locks across async await unless the design and primitive explicitly support it.

Tests can observe cleanup with counters or mock guards, while compile-fail evidence protects impossible escaping borrows. Together they cover memory and operational lifetime.

Read the diagnostic as an ownership trace

I treat the “temporary value” label as the start of a trace. I write down the owner-producing expression, the borrowing conversion, and the final use of the reference. Usually one of those three positions reveals the intended boundary. Naming the owner fixes the case only when that owner can correctly live until the last use; otherwise returning an owned result is the honest repair. This avoids cargo-culting an extra let into code where lifetime and responsibility should move to the caller.

My E0716 checklist

  • Which expression creates the temporary owner?
  • What returned value still borrows from it?
  • At which statement or documented temporary scope is it destroyed?
  • Can a named local keep the owner alive for the whole use?
  • Must the result cross the function or task boundary as owned data?
  • Is the code relying on a narrow temporary lifetime-extension rule?
  • Did a method chain hide an as_, iterator, or getter borrow?
  • Does temporary drop timing release locks or other resources too early or late?

The core principle is that borrowed data remains tied to its concrete owner even when helper calls hide the path. E0716 makes that hidden temporary visible. I name or transfer ownership according to the real operation instead of stretching a view beyond its storage.