The reference points to a ghost
You write a function to extract a name from a user object. You return a &str. The compiler screams. You look at the code, and the name is right there. Why does Rust think it is gone? This happens when a reference points to data that gets dropped before the reference is finished using it. The compiler is protecting you from a dangling pointer. A dangling pointer references memory that has already been freed. Accessing that memory causes crashes, data corruption, or security vulnerabilities. Rust prevents this at compile time. You cannot run code that contains a dangling reference.
The lease and the building
Think of a reference like a lease on an apartment. The lease gives you permission to live there. The apartment building is the data. If the building is demolished while your lease is still active, you have nowhere to live. The lease is invalid. The lease points to a ghost. Rust checks the lease dates against the building demolition schedule. If the demolition happens before the lease expires, the contract is rejected. The data must survive as long as the reference exists.
In Rust, every variable has a lifetime. The lifetime is the range of code where the variable is valid. When you create a reference, the compiler checks if the referenced variable lifetime covers the reference lifetime. If the variable dies inside a block, and the reference escapes that block, the check fails. The error "borrowed value does not live long enough" means the source data expires before the borrower is finished.
Minimal example: escaping the block
The simplest case is a variable created inside a block that you try to borrow outside that block. Braces create scopes. When a scope ends, variables are dropped.
fn main() {
// We declare the reference here, so it lives until the end of main.
let long_lived_ref;
{
// data is created here. Its lifetime starts now.
let data = String::from("temporary");
// We try to borrow data and store the reference outside the block.
// The compiler sees that data will die when the block ends.
long_lived_ref = &data;
}
// The block ends. data is dropped immediately.
// long_lived_ref now points to freed memory.
// The compiler rejects this with E0597.
println!("{}", long_lived_ref);
}
The variable data exists only inside the braces. When the closing brace is reached, data is destroyed. The reference long_lived_ref tries to escape the block. The compiler sees that data dies before long_lived_ref is used. The error E0597 tells you the borrowed value does not live long enough. The fix is to keep the data alive. Move the creation of data outside the block, or use the reference before the block ends.
Trust the borrow checker. It usually has a point.
Functions and local variables
Functions are the most common trap. A function creates a new scope. Local variables inside the function are destroyed when the function returns. If you return a reference to a local variable, you are returning a reference to dead memory.
/// This function fails because it returns a reference to a local variable.
fn broken_greeting() -> &str {
// message is local to this function.
// Its lifetime ends when the function returns.
let message = String::from("Hello");
// We return a reference to message.
// The caller receives a pointer to memory that is about to be freed.
&message
}
The compiler emits E0515: cannot return reference to local variable. The lifetime of message ends at the return statement. The caller receives a reference that points to nothing. The fix depends on what you need. If the function creates the data, return the owned value. If the function extracts data from an argument, return a reference to the argument.
/// Return the owned String. The caller takes ownership.
/// The caller is responsible for dropping the data.
fn fixed_greeting_owned() -> String {
String::from("Hello")
}
/// Borrow the input. The reference lifetime is tied to the input.
/// The caller guarantees the input lives long enough.
fn extract_first_char(s: &str) -> &str {
// s lives at least as long as the return value.
// The compiler infers that the return reference cannot outlive s.
&s[..1]
}
Returning owned types transfers responsibility. The caller owns the data and decides when to drop it. Returning references ties the output lifetime to an input lifetime. The compiler enforces that the input stays alive.
If you cannot prove the data lives, make it live.
Structs and lifetime annotations
Structs can hold references. The struct itself has a lifetime. The struct cannot outlive the data it references. This is enforced by lifetime annotations. The annotation tells the compiler how the struct lifetime relates to the reference lifetime.
/// A note that borrows text from somewhere else.
/// The lifetime parameter 'a ties the struct to the reference.
struct Note<'a> {
text: &'a str,
}
fn main() {
let note;
{
// content is local to this block.
let content = String::from("Important");
// note borrows content.
// The compiler infers that note cannot outlive content.
note = Note { text: &content };
}
// content is dropped. note is invalid.
// The compiler rejects this usage.
println!("{}", note.text);
}
The error points to the assignment or the usage. The lifetime 'a on Note says the struct lives as long as the reference. If content dies, note cannot exist. Move content to a scope that covers note, or change Note to own the data.
/// A note that owns its text.
/// No lifetime parameter is needed.
struct OwnedNote {
text: String,
}
fn main() {
let content = String::from("Important");
// Clone the string so the note owns its copy.
let note = OwnedNote { text: content.clone() };
// content can be dropped safely. note has its own data.
drop(content);
println!("{}", note.text);
}
Owning the data removes the lifetime constraint. The struct manages the memory. This is often the right choice when the data needs to live independently. Cloning adds a small allocation cost. Weigh that cost against the complexity of managing lifetimes.
Convention aside: &'static str is special. String literals live for the entire program. You can return &'static str from any function. The lifetime 'static means the data is embedded in the binary and never gets dropped. This is the most efficient way to return fixed strings.
/// Returns a reference to a string literal.
/// The literal lives for the duration of the program.
fn get_constant() -> &'static str {
"constant"
}
Pitfalls and error codes
You will encounter a few specific error codes for this family of problems. E0597 is the general case. The borrowed value does not live long enough. E0515 is specific to functions. You cannot return a reference to a local variable. E0517 is related. A cast requires a temporary value to live long enough. All of these mean the same core problem. The data is too short-lived.
Confusing String and &str is a frequent source of pain. String owns data. &str borrows data. If a function signature asks for &str, you can pass a String reference. If it returns &str, you cannot return a reference to a local String. The return type dictates the contract. If you need to return data created inside the function, the return type must be owned.
Closures capture references by default. If a closure captures a variable, it borrows that variable. The closure cannot outlive the variable. This causes the same error when the closure is stored or returned.
fn main() {
let closure;
{
let data = String::from("captured");
// The closure captures a reference to data.
// The closure cannot outlive data.
closure = || println!("{}", data);
}
// data is dropped. closure is invalid.
closure();
}
The fix is to move the data into the closure. This transfers ownership to the closure. The closure owns the data and keeps it alive.
fn main() {
let closure;
{
let data = String::from("captured");
// Move data into the closure.
// The closure now owns data.
closure = move || println!("{}", data);
}
// data is gone from the stack, but the closure owns it.
// The closure keeps data alive.
closure();
}
The move keyword changes the capture semantics. The closure takes ownership of the captured variables. This is essential when the closure outlives the scope where it is created.
Return ownership, not a ghost.
Decision matrix
Return owned types like String or Vec<T> when the function creates new data and the caller needs to keep it. Ownership transfers cleanly across function boundaries. The caller manages the lifetime. Use references like &str or &[T] when the function processes data provided by the caller. The caller guarantees the data stays alive. The function just looks at it. Use Cow<str> when you sometimes return a reference to existing data and sometimes return a newly computed string. Cow avoids allocation when borrowing is possible. Use &'static str for constant text that never changes. This is the most efficient way to return fixed strings. Use move closures when the closure must outlive the variables it captures. The closure takes ownership and keeps the data alive.