Skip to content
← All articles

When elision fails: reading E0106

E0106 is the first hard wall almost everyone hits, and the natural response — paste `'a` until it compiles — is the wrong one. It is a question with a right answer that depends on your intent. Here is every shape it comes in, and how the body decides.

error[E0106]: missing lifetime specifier
              expected named lifetime parameter

This is the wall. Nearly everyone hits it in their first week of writing functions that return references, and nearly everyone’s first instinct is to sprinkle <'a> around until the red goes away.

Resist. E0106 is a question, and it has a right answer that depends on what you meant. Getting into the habit of answering it deliberately is the difference between understanding lifetimes and cargo-culting them for years.

The question is always the same one:

Where do the bytes behind the reference I am returning actually come from?

Answer that, and the annotation writes itself. If the honest answer is “from something I own and am about to drop”, then there is no annotation, and the error is telling you your design is wrong rather than your syntax.

The five shapes

1. No input lifetimes at all

fn get_str() -> &str {
    // error[E0106]: missing lifetime specifier
    //   this function's return type contains a borrowed value,
    //   but there is no value for it to be borrowed from
    "hi"
}

The compiler’s note is unusually direct: there is nothing to borrow from. No parameter carries a lifetime, so there is no candidate for the output.

This shape has no annotation-only fix. Writing fn get_str<'a>() -> &'a str compiles the signature but makes an impossible promise — the caller picks 'a, and the body must produce data valid for whatever they picked. Your two real options are:

  • Return String — own the data, hand it over, no lifetimes involved.
  • Return &'static strif and only if the data genuinely is static: a string literal, a const, a static. fn get_str() -> &'static str { "hi" } is perfectly correct, because "hi" really does live for the whole program.

This is where learners first reach for 'static as a magic word. It is not magic; it is a specific, checkable claim. Make it only when it is true.

2. Two inputs, elided output

fn frob(s: &str, t: &str) -> &str {
    // error[E0106]
    // help: this function's return type contains a borrowed value, but the
    //       signature does not say whether it is borrowed from `s` or `t`
}

Two lifetime-carrying parameters, so rule 2 cannot fire. The help: here spells the question out for you. Answer it from the body.

Note what rustc then suggests:

help: consider introducing a named lifetime parameter
  |
1 | fn frob<'a>(s: &'a str, t: &'a str) -> &'a str

It ties both parameters to 'a, because that is the only suggestion that is always sound. It is frequently not the one you want. This shape has two different correct answers, and choosing wrong is a real API bug rather than a stylistic one. That is the next section.

3. A struct or enum field holding a reference

struct Excerpt { part: &str }
//                     ^ error[E0106]

A type that holds a reference must declare the lifetime it holds it for:

struct Excerpt<'a> { part: &'a str }

And now Excerpt is no longer a type — Excerpt<'a> is. That has consequences which get their own article; they are the second-biggest stall point in the topic.

4. A type alias

type MyStr = &str;
//           ^ error[E0106]

Same rule, no receiver, no parameters, nothing to infer from. Write type MyStr<'a> = &'a str;.

5. A path to a lifetime-carrying type

struct View<'a> { s: &'a str }

fn make() -> View {
//           ^ error[E0106]
}

View is a path that carries a lifetime, even though nothing about the token View says so. This “hidden” spelling is legal in some positions and confusing in most; the lint that polices it gets its own article.

💡For each of these, say whether E0106 fires and, if it does, what the honest answer is. click to reveal
fn one(config: &Config) -> &str;
fn two(name: &str, sep: char) -> &str;
fn three() -> &[u8];
fn four(a: &str, b: &str) -> String;

one — no error. One lifetime-carrying parameter, so the output takes its lifetime. The returned &str borrows from the config, which is exactly what you would want.

two — no error. char carries no lifetime, so name is the only carrier. Elision fires.

three — E0106, shape 1. Nothing to borrow from. If the bytes are a literal or a const, say -> &'static [u8]. If they are computed, return Vec<u8>.

four — no error, and no lifetime anywhere. The return type is owned. This is the quiet reminder that the whole problem disappears when you stop returning references — which is sometimes exactly the right call, and is always worth considering before you start annotating.

The “fix” that compiles and is still wrong

Back to shape 2. Here is the fix nearly everyone applies:

fn frob<'a>(s: &'a str, t: &'a str) -> &'a str { ... }

It compiles. Tests pass. And if the returned slice actually comes only from s, it is a design bug that will annoy every caller you have.

Remember what 'a on both parameters means: the result is valid only over a region where both inputs are valid. So this ordinary caller stops compiling:

let haystack = String::from("hello world");
let found;
{
    let needle = String::from("wor");
    found = frob(&haystack, &needle);
}
println!("{found}");
// error[E0597]: `needle` does not live long enough

The caller built a scratch value, used it, dropped it, and wanted to keep the result — which points into haystack, not into needle. Your signature said otherwise, so the compiler believes your signature.

This is the number-one lifetime design smell in real crates. The correct answers are:

// the result comes from `s`:
fn frob<'a>(s: &'a str, t: &str) -> &'a str

// the result comes from either, decided at runtime:
fn frob<'a>(s: &'a str, t: &'a str) -> &'a str

Both are legitimate. The body decides which. If every return in the body produces a slice of s, the first is right and the second over-constrains. If some paths return a slice of t, the second is the only option.

💡Which signature does each of these bodies want, and why can't the compiler work it out for you? click to reveal
// A
fn pick(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}

// B
fn strip(text: &str, prefix: &str) -> &str {
    text.strip_prefix(prefix).unwrap_or(text)
}

A wants <'a>(a: &'a str, b: &'a str) -> &'a str. Both branches can return, so the result must be valid wherever both are — the constraint is real, not gratuitous.

B wants <'a>(text: &'a str, prefix: &str) -> &'a str. Every path returns a slice of text; prefix is only ever read. Tying prefix to 'a would force callers to keep their prefix alive for as long as they hold the result, for no reason at all.

Why can’t the compiler infer it? Because a signature is a contract with callers, and the compiler type-checks call sites against the signature alone. If it derived lifetimes from the body, then changing the body could silently break every caller — the classic argument for why type signatures are written, not inferred, at API boundaries. Rust makes the same trade for lifetimes.

There is a deeper point hiding here, and it is worth naming: a signature can compile and still be wrong. The compiler verifies soundness, never intent. Nothing in A‘s over-tied form is unsound; it is merely a promise you did not need to make and cannot take back without a breaking change.

The checklist

When you see E0106:

  1. Find the returned reference. Ask where its bytes come from.
  2. If they come from exactly one parameter — give that parameter 'a, give the output 'a, and leave every other parameter alone.
  3. If they can come from more than one — give 'a to each parameter that can be the source, and to the output.
  4. If they come from a local — stop. No annotation exists. Change the return type, or change who owns the storage.
  5. If there are no parameters at all — return an owned value, or &'static if the data really is static.

Note that steps 2 and 3 are the same mechanical move applied to a different answer to step 1. All the thinking is in step 1.