Skip to content
← All Rust articles

Rust explainer

Make borrowed return types visible with lifetime syntax

About 4 min to read. Allow extra time to work through examples.

Understand hidden, elided and named lifetimes, fix inconsistent signatures, and distinguish a style warning from a borrowing error.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A function can borrow data even when its return type shows no reference. Consider this valid function, which triggers a compiler warning:

pub fn items(scores: &[u8]) -> std::slice::Iter<u8> {
    scores.iter()
}

Iter has both a lifetime parameter and an element type. Writing Iter<u8> hides the lifetime parameter. The mismatched_lifetime_syntaxes warning asks you to make its relationship to the input easier to see. With warnings denied, as in this site's Rust checks, the warning becomes an error.

Add '_ to the return type:

pub fn items(scores: &[u8]) -> std::slice::Iter<'_, u8> {
    scores.iter()
}

This changes the notation, not what the function borrows or how long the data lives. The iterator still borrows the input slice.

Three forms of lifetime syntax

Form Examples What is visible?
Named &'a str, View<'a> An explicit lifetime name
Elided &str, &'_ str, View<'_> A reference or lifetime placeholder
Hidden View, when declared as View<'a> No visible lifetime argument in the path

The lint checks inconsistent forms for the same lifetime across a function's input and output. It does not require every lifetime in a signature to have the same name or syntax. In particular, &str and &'_ str belong to the same group. See the compiler's lint guide.

'_ asks Rust to infer the lifetime. In this example, the single input lifetime determines the output lifetime under the elision rules. A placeholder does not remove borrowing restrictions or resolve every ambiguous signature. A function borrowing from either of two independent inputs may still need named lifetimes to state the relationship.

Check the specific warning

These are complete function definitions. Which ones trigger mismatched_lifetime_syntaxes?

pub struct View<'a> {
    pub text: &'a str,
}

pub fn first<'a>(view: &'a View<'a>) -> &str {
    view.text
}

pub fn second(view: View) -> View {
    view
}

pub fn third(text: &str) -> std::str::Chars<'_> {
    text.chars()
}

pub fn fourth<'a>(text: &'a str) -> std::str::Chars<'a> {
    text.chars()
}

first triggers the warning: its named input lifetime is elided in the return type. second uses hidden lifetimes on both sides. third uses elided forms on both sides, and fourth uses a name consistently. Those three do not trigger this particular warning.

These answers concern this particular lint. A clean result for it does not establish that a larger program passes every compiler and Clippy check. For second, writing View<'_> on both sides makes the borrowing visible even though this compiler lint does not require the change.

See what the return value borrows

Here is a complete example with a borrowed configuration view:

pub struct Config<'a> {
    pub root: &'a str,
}

pub fn load(text: &str) -> Config<'_> {
    Config { root: text }
}

fn main() {
    let text = String::from("assets");
    let config = load(&text);
    assert_eq!(config.root, "assets");
}

In this implementation, config.root refers into text. You cannot destroy text and then use that reference. The borrow checker enforces this even if you hide the return type's lifetime.

Choose a borrowed view when sharing the caller's text fits the API. Store a String instead when the result should own its text independently. The lint does not decide which design is appropriate. Nor does a lifetime parameter by itself prove that every implementation returns data from that input: a reference to static data could also satisfy this signature.

Apply the fix

Read the diagnostic's input and output spans. For a simple inferred borrow, add '_ in the return type's lifetime slot, as in Iter<'_, u8>. If a name is needed to express a relationship, use that name consistently instead.

Check the type's declaration for argument order. std::str::Split<'_, char> has a lifetime argument followed by its pattern type. Writing Split<'_> omits that required type argument.

The separate elided_lifetimes_in_paths lint warns about hidden path lifetimes even when used consistently. It is allow-by-default on the checked toolchain; projects can enable it explicitly. Use mismatched_lifetime_syntaxes when configuring the consistency lint rather than copying an older lint name.

You can also return impl Iterator<Item = &u8> + '_ to hide the concrete iterator type. That changes the public API: callers only get the capabilities promised by the return type, rather than all of slice::Iter's methods and iterator traits. Choose it for that API design, not merely to silence a warning.

step 14 of 22

Ownership III: Lifetimes, Explicitly

Up next Make the lint happy: '_ in return paths