Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-055 · Case file with fixtures · Case 27 of 694 · Compiler evidence

E0716: Why a Rust Temporary Is Dropped While Its Borrow Is Still Used

A temporary owner usually dies at the end of its statement, while a reference stored in a local survives into the next one. Name the owner and make its required scope explicit.

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

Direct answer

What this Rust failure means

Why it happens
The owned temporary is normally destroyed at the end of the statement, while the derived reference is stored and used by a later statement.
First discriminating check
Give the temporary owner its own local binding, borrow from that binding on the next line, and compare the resulting drop scope.

E0716 often appears in a line that looks nicely compact:

let bytes = String::from("atlas").as_bytes();
println!("{bytes:?}");

Rust 1.98.1 rejects the first line because the String is a temporary owner. as_bytes borrows from it, and bytes stores that reference for the next statement. The owner, however, would normally be dropped at the semicolon.

The failing fixture produces the exact diagnostic:

error[E0716]: temporary value dropped while borrowed

This is close to E0515, but the boundary is different. E0515 tries to return a reference beyond a function. E0716 can happen entirely inside one function because a temporary's destruction scope is shorter than the local receiving its borrow.

Write the two lifetimes as a timeline

The original expression performs several operations:

  1. Allocate a temporary String.
  2. Borrow its byte slice.
  3. Store the slice in bytes.
  4. Reach the end of the statement and drop the temporary String.
  5. Attempt to use bytes in the next statement.

Step four would make step five invalid. The compiler prevents the dangling slice.

The Rust Reference section on temporary scopes defines where temporaries are normally destroyed and lists contexts where their lifetime is extended. I do not rely on remembering every extension rule while debugging. I name the owner when a borrow needs to cross a statement.

Name the owner before naming the view

The direct repair is:

let text = String::from("atlas");
let bytes = text.as_bytes();
println!("{bytes:?}");

Locals are dropped at the end of their enclosing scope, generally in reverse declaration order. bytes stops being used before text is dropped, so the reference is valid. The repaired fixture is compiled and executed under the recorded toolchain.

This extra binding is valuable documentation. It tells a reviewer which value owns the memory and which value only views it.

Why some similar let expressions work

Rust extends temporary lifetimes in selected syntactic contexts. For example, directly binding a reference can keep a temporary alive longer:

let text = &String::from("atlas");
println!("{text}");

But lifetime extension is syntax-directed. Calling a method and storing the reference returned by that method does not imply that every temporary receiver is extended to the local's scope. Small refactors can therefore change whether an expression matches an extension rule.

This is why I prefer an owner binding for non-trivial code. It is stable under method extraction, added adapters, and changes to the borrowed view.

Common real forms

The same issue appears beyond String::as_bytes:

let name = config().service_name();
let path = PathBuf::from(input).as_path();
let guard = shared.clone().lock().unwrap();

Each line should trigger the same question: does the method return owned data, or a reference tied to its receiver? If it returns a reference and the receiver is temporary, the reference may outlive its owner.

The mutex form deserves care. Naming the cloned Arc can extend the owner, but a guard also controls how long the lock remains held. A repair must preserve both memory validity and the intended synchronization boundary.

Do not repair it with an accidental allocation

Calling to_owned, to_vec, or to_string on the borrowed result can make it independent from the temporary:

let bytes = String::from("atlas").as_bytes().to_vec();

This is correct if bytes should own a copy. It is not equivalent to extending the original owner: it allocates and copies. In a hot path, that distinction matters. I first decide whether the consumer needs ownership or only a view.

Parentheses do not extend a lifetime, and writing an explicit lifetime annotation on bytes cannot change the temporary's drop point. The repair must either keep the owner alive or create independent owned data.

My discriminating check

For a long expression, I split only the receiver into a local. If the compiler error disappears, I have proven that temporary ownership was the relevant boundary. I then choose the final design:

  • Keep the owner local and pass a view.
  • Move the owner into a longer-lived structure.
  • Return or store owned data.
  • Use a genuine static value when the data is fixed.

The official E0716 explanation includes temporary lifetime extension examples. The operational lesson is short: when a reference survives a semicolon, make the value behind that reference visible. The resulting code is normally easier for both the compiler and the next engineer to reason about.