The reference that points to empty space
You write a function that calculates a path, stores it in a local variable, and tries to hand back a reference to that variable. The compiler immediately rejects the code. It does not care that your logic looks sound. It does not care that you only plan to read the data once. It sees a reference trying to outlive the memory it points to, and it stops the build.
This is the dangling reference problem. In languages without memory safety, a dangling reference points to memory that has already been freed. Accessing it causes a crash, a silent data corruption, or a security vulnerability. Rust eliminates the problem entirely by refusing to compile code that could create one. The compiler enforces a strict rule: a reference cannot outlive the data it points to.
How Rust tracks what lives and what dies
Think of a reference like a ticket to a ride at an amusement park. The ticket is only valid while the ride is operating. If the ride shuts down for the night, the ticket becomes worthless. Handing someone a ticket after the ride closes is a dangling reference.
Rust solves this with a system called lifetimes. The compiler does not guess. It maps out exactly when every variable is created and when it is destroyed. It draws invisible boundaries around scopes. If a reference tries to cross one of those boundaries and outlive its target, the compiler throws an error. You never have to manually track memory. The borrow checker does the accounting for you at compile time.
The minimal failing case
Here is the simplest way to trigger the error. A reference is declared outside a block, but it points to a variable created inside that block.
fn main() {
let r; // r is declared in the outer scope and needs to survive until the end of main
{
let x = 5; // x is created in the inner scope
r = &x; // r tries to borrow x, which violates the scope boundary
} // x is dropped here. The memory for x is reclaimed.
println!("r = {r}"); // r tries to use x after it is gone
}
The compiler rejects this immediately. It does not run the program to check. It analyzes the scopes and sees that x dies at the closing brace, but r needs to stay alive until the end of main. The error message points directly to the line where the borrow happens and explains the lifetime mismatch.
Trust the borrow checker here. It catches the problem before a single byte of memory is allocated.
What the compiler actually checks
The borrow checker runs a static analysis pass over your code. It assigns a lifetime to every reference. A lifetime is just a label that represents a span of code where the reference is valid. When you write &x, the compiler attaches a lifetime to that reference. When you assign it to r, the compiler checks whether r's lifetime fits inside x's lifetime.
In the example above, x has a short lifetime. r has a long lifetime. The compiler sees that r's lifetime extends past x's lifetime. It fails the check. This happens before any machine code is generated. You get a clear error message instead of a runtime crash.
The compiler also tracks drop order. When a variable goes out of scope, Rust calls its destructor automatically. If a reference still points to that variable, the destructor would free the memory while the reference still expects it to be there. The borrow checker prevents this ordering problem by enforcing that references cannot outlive their owners.
Lifetime elision and hidden rules
Rust hides lifetime syntax in most everyday code through a set of elision rules. You rarely write &'a T in function signatures. The compiler fills in the missing lifetimes automatically. This keeps your code readable while preserving the same safety guarantees.
The rules are straightforward. If a function has exactly one input reference, the compiler assigns that lifetime to all output references. If a function has multiple input references and one of them is &self, the compiler assigns the lifetime of self to all output references. If none of those apply, the compiler requires you to write the lifetimes explicitly.
Understanding elision helps you read compiler errors faster. When the compiler complains about a missing lifetime parameter, it usually means your function signature violates the elision rules. You either need to add explicit lifetime annotations, or you need to change the function to return an owned type instead.
Realistic scenario: returning data from a function
The dangling reference problem shows up most often when writing functions. You create data inside a function and try to return a reference to it.
fn get_message() -> &str {
let text = String::from("Hello"); // text is created on the heap inside this function
&text // trying to return a reference to text
} // text is dropped here. The reference points to nothing.
This fails with a lifetime mismatch error. The compiler sees that text is a local variable. It will be dropped when the function returns. The function signature promises to hand back a reference that lives longer than the function call. Those two facts contradict each other. The compiler emits E0515 (returned value does not live long enough) and highlights the return line.
The fix is usually to return the owned value instead of a reference.
fn get_message() -> String {
let text = String::from("Hello");
text // returns the owned String and moves ownership to the caller
}
Returning the owned value moves the data out of the function. The caller becomes the new owner. The memory stays alive as long as the caller needs it. This pattern is so common that Rust developers call it "returning by value." It avoids lifetime annotations entirely and keeps the API simple.
Structs that hold references
The same rule applies when you store references inside structs. If a struct holds a reference, the struct cannot outlive the data it references.
struct Document<'a> {
title: &'a str, // the struct borrows a string slice with lifetime 'a
}
fn create_document() -> Document {
let title = String::from("Notes"); // title is local to this function
Document { title: &title } // trying to store a reference to a local variable
} // title is dropped. The struct now holds a dangling reference.
The compiler rejects this with E0515. The struct signature implies it can live as long as the data it points to. The function creates the data locally and destroys it immediately. The compiler blocks the mismatch.
You fix this by either returning an owned struct, or by passing the data into the function so the caller controls the lifetime.
fn create_document(title: &str) -> Document {
Document { title } // title is borrowed from the caller, so it lives long enough
}
Passing the data in shifts the lifetime responsibility to the caller. The compiler verifies that the caller's data outlives the struct. This is the standard pattern for builders and parsers.
Pitfalls and compiler signals
The compiler gives you specific error codes when lifetimes clash. E0515 means a returned value does not live long enough. E0597 means a borrowed value does not live long enough. Both point to the same underlying issue. A reference is trying to outlive its data.
When you see these errors, check the scope boundaries. Look at where the data is created and where the reference is used. If the data is created inside a block or a function, you cannot hand out a reference to it. You must either return the owned data, or pass the data into the function so the caller controls its lifetime.
Temporary values cause a related trap. String literals live for the entire program duration, so &"hello" is always safe. Temporary String objects created on the fly are different. If you write let s = &String::from("temp"), the compiler emits E0716 (temporary value dropped while borrowed). The temporary string is created and destroyed in the same statement. The reference has nowhere to point. The fix is to bind the temporary to a variable first, or return the owned string directly.
There is one exception. Raw pointers created with unsafe bypass the borrow checker. You can create a raw pointer to a local variable, let the variable drop, and keep using the pointer. This creates a genuine dangling reference. The compiler will not stop you. The community treats this as a last resort. You only use raw pointers when interfacing with C code or implementing low-level data structures. Even then, you wrap the unsafe code in a safe API so the rest of your program cannot accidentally touch the dangling pointer.
Convention aside: when you fix a lifetime error by returning an owned type, rename the function if the name implies borrowing. A function named get_data suggests it returns a reference. A function named build_data or create_data signals that it returns ownership. Clear names prevent future confusion. Another convention is to prefer &str over &String in function signatures. It accepts both string slices and borrowed strings without forcing the caller to allocate.
Choosing the right approach
Use owned types when the function creates the data and the caller needs to keep it. Returning a String, Vec, or custom struct moves ownership to the caller and eliminates lifetime tracking.
Use references when the data already exists and the caller controls its lifetime. Pass the data into the function and return a reference to a slice or a subset of the original data. The borrow checker guarantees the reference stays valid.
Use Rc<T> or Arc<T> when multiple parts of your program need to share ownership of the same data. Reference counting keeps the data alive as long as anyone holds a handle to it. The compiler tracks the count automatically.
Use unsafe raw pointers only when you are writing a foreign function interface or a memory allocator. You must document every invariant and guarantee that the pointer is never dereferenced after the backing memory is freed.