Error E0621

"explicit lifetime required" — How to Fix

Fix Rust Error E0621 by adding explicit lifetime parameters to function signatures to define how long references remain valid.

The compiler refuses to guess

You write a function that compares two string slices and returns the longer one. It looks perfectly logical. You pass it two &str arguments and return a &str. You hit compile and Rust immediately throws E0621: explicit lifetime required in the return type. The compiler stops you because it genuinely does not know where your returned pointer points. It sees two input references with unknown validity windows and one output reference. Without a contract, it cannot guarantee the output will not dangle.

This error is not a bug. It is the borrow checker enforcing memory safety by refusing to make assumptions about reference relationships. You have to tell the compiler which input the output is tied to. Once you add the lifetime annotation, the error disappears and the code compiles. The annotation does not change how long anything lives. It only describes the relationship between inputs and outputs so the compiler can verify safety at compile time.

What lifetimes actually track

Lifetimes are compile-time tags that link references to the data they point at. They do not measure time in seconds or milliseconds. They measure scope. When you write &'a str, you are telling the compiler that this reference is valid for some scope named 'a. Every reference that carries the same lifetime tag must outlive that scope. The compiler uses these tags to prove that a reference will never point to freed memory.

Think of a lifetime like a citation in an academic paper. When you quote a passage, you must cite the source. The citation does not keep the source book from being destroyed. It simply records which book the quote came from so readers can verify it exists. If the source book is burned down, the citation becomes invalid. Rust requires every returned reference to carry a citation that points to a live input. E0621 fires when you return a reference without providing that citation.

The compiler tracks lifetimes through a process called borrow checking. It walks your function signature, matches lifetime parameters, and verifies that every output lifetime is a subset of an input lifetime. If the relationship is ambiguous, compilation fails. You resolve the ambiguity by naming the lifetime and attaching it to the relevant parameters.

The minimal fix

The broken function looks like this:

/// Returns the longer of two string slices.
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The compiler rejects this with E0621. It sees x and y as independent references. It sees the return type as an independent reference. It cannot assume the return value comes from x, from y, or from some hidden static string. You must draw the connection explicitly.

/// Returns the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // The lifetime parameter 'a' ties all three references together.
    // The compiler now knows the output lives as long as the shorter input.
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The 'a is a lifetime parameter. It works like a generic type parameter, but for references instead of values. You declare it after the function name in angle brackets. You attach it to every reference that shares the same validity window. The compiler reads this as a contract: the returned reference will not outlive either x or y. If you try to return a reference to a local variable, the compiler will catch it because the local variable's scope is shorter than 'a.

Convention aside: developers almost always name simple lifetime parameters 'a. You can use 'input, 'data, or 'ctx if the function has multiple distinct lifetime groups, but 'a is the standard for single-group signatures. Stick to 'a until you actually need to distinguish between two independent reference scopes.

Why the compiler stops you here

Rust includes lifetime elision rules that let you omit lifetime annotations in common cases. The compiler automatically infers lifetimes when there is exactly one input reference, or when the function is a method with &self or &mut self. Those rules cover the majority of simple functions. They deliberately exclude functions with multiple input references.

The elision rules stop at multiple inputs because the compiler cannot safely guess which input the output depends on. Consider a function that takes a configuration string and a user string, then returns a slice from one of them. If the compiler assumed the output lived as long as the first input, but your code actually returned a slice from the second input, you would get a dangling pointer when the first input drops. The compiler refuses to take that risk. It demands an explicit annotation so you, the author, declare the relationship.

This is where the ah-ha moment usually lands: lifetimes are not durations. They are constraints. Writing &'a str does not make the string live longer. It only says "this reference is valid for whatever scope 'a represents." The actual lifetime is determined by where the data is created and where it goes out of scope. The annotation is a proof obligation, not a magic wand.

A realistic scenario

Real code rarely deals with abstract x and y variables. It deals with configuration, user input, and fallback values. Here is a practical function that picks a display name from a user profile or falls back to a system default.

/// Picks a display name from user input or falls back to a default.
fn pick_display_name<'a>(user_input: &'a str, fallback: &'a str) -> &'a str {
    // Trim whitespace to validate the input.
    let trimmed = user_input.trim();
    
    // Return the user input if it contains actual text.
    if !trimmed.is_empty() {
        trimmed
    } else {
        // Fall back to the default when the input is blank.
        fallback
    }
}

The lifetime annotation 'a ties user_input, fallback, and the return value together. The compiler verifies that trimmed is a slice of user_input, so it inherits the same lifetime. The function returns either a slice of user_input or the fallback reference. Both paths satisfy the 'a contract. If you tried to return a reference to a String created inside the function, the compiler would reject it with E0515 (cannot return value referencing local variable) because the local String would drop at the end of the function, violating the 'a constraint.

Convention aside: keep lifetime annotations on the signature, not inside the function body. The body does not need lifetime tags. The signature is the contract. The implementation just has to honor it.

Common traps and compiler follow-ups

E0621 is usually the first error in a chain. Once you fix the signature, you might immediately hit E0597: borrowed value does not live long enough. This happens when the caller passes a temporary value or a variable that drops before the returned reference is used. The compiler is doing its job. It caught a dangling reference before it could cause undefined behavior.

Another frequent trap is over-annotating. You do not need to attach 'a to every reference in a function. Only attach it to references that share the same validity window. If a function takes a configuration reference and a mutable buffer, and returns a reference tied only to the buffer, the configuration reference gets its own lifetime parameter. Forcing everything into 'a creates unnecessary constraints that make the function harder to call.

Developers also confuse lifetimes with ownership. A lifetime annotation does not transfer ownership. It only tracks borrowing. If you need the data to survive beyond the current scope, you must return an owned type like String or Vec<T>. Lifetimes cannot extend the life of a value. They can only describe how long a borrow is valid.

The compiler will sometimes suggest a lifetime fix in its error message. Read the suggestion carefully. It usually points to the exact parameter that needs the annotation. If the suggestion conflicts with your intended logic, your function signature is modeling the wrong relationship. Step back and verify which input the output actually depends on.

When to reach for explicit lifetimes

Use explicit lifetimes when a function takes multiple input references and returns a reference, because the compiler cannot safely guess which input the output depends on. Use explicit lifetimes when you are building a data structure that stores references, because the struct definition must declare how long the stored references remain valid. Use explicit lifetimes when you are writing iterator adapters or parser combinators that chain references through multiple stages, because each stage needs a clear contract about validity windows. Reach for lifetime elision when there is only one input reference or when the function is a method with &self, because the compiler handles those cases automatically. Reach for owned types like String or Vec<T> when the data must outlive the current scope or when you need to mutate the collection without tracking complex borrow relationships. Reach for Cow<str> when you want to accept either borrowed or owned data and return a unified type, because it lets you avoid cloning when borrowing is possible while still allowing ownership transfer when necessary.

Lifetimes are contracts, not timers. Write them to describe reality, not to force the compiler to accept unsafe patterns. Trust the borrow checker. It usually has a point.

Where to go next