Rust-2024

65. Precise Capturing — Stop impl Trait From Borrowing Too Much

Ever had the compiler refuse to let you use a value after calling a function — even though the return type shouldn’t borrow it? use<> bounds give you precise control over what impl Trait actually captures.

The overcapturing problem

In Rust 2024, impl Trait in return position captures all in-scope generic parameters by default — including lifetimes you never intended to hold onto.

Here’s where it bites:

1
2
3
fn make_greeting(name: &str) -> impl std::fmt::Display + use<> {
    format!("Hello, {name}!")
}

Without the use<> bound, the returned impl Display would capture the &str lifetime — even though format! produces an owned String that doesn’t borrow name at all. That means the caller can’t drop or reuse name while the return value is alive, for no good reason.

Enter use<> bounds

Stabilized in Rust 1.82, the use<> syntax lets you explicitly declare which generic parameters the opaque type is allowed to capture:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn make_greeting(name: &str) -> impl std::fmt::Display + use<> {
    format!("Hello, {name}!")
}

fn main() {
    let mut name = String::from("world");
    let greeting = make_greeting(&name);

    // This works! The greeting doesn't capture the lifetime of `name`
    name.push_str("!!!");

    println!("{greeting}"); // Hello, world!
    println!("{name}");     // world!!!
}

use<> means “capture nothing” — the return type is fully owned and independent of any input lifetimes.

Selective capturing

You can also pick exactly which lifetimes to capture. Consider a function that takes two references but only needs to hold onto one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn pick_label<'a, 'b>(
    label: &'a str,
    _config: &'b str,
) -> impl std::fmt::Display + use<'a> {
    // We use `_config` to decide formatting, but
    // the return value only borrows from `label`
    format!("[{label}]")
}

fn main() {
    let label = String::from("status");
    let mut config = String::from("uppercase");

    let display = pick_label(&label, &config);

    // We can mutate `config` — the return value doesn't borrow it
    config.push_str(":bold");

    assert_eq!(format!("{display}"), "[status]");
    assert_eq!(config, "uppercase:bold");
}

use<'a> says “this return type borrows from 'a but is independent of 'b.” Without it, the compiler assumes the opaque type captures both lifetimes, preventing you from reusing config.

When you need this

The use<> bound shines when you’re returning iterators, closures, or other impl Trait types from functions that take references they don’t actually need to hold:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn make_counter(start: &str) -> impl Iterator<Item = usize> + use<> {
    let n: usize = start.parse().unwrap_or(0);
    (n..).take(5)
}

fn main() {
    let mut input = String::from("3");
    let counter = make_counter(&input);

    // We can mutate `input` because the iterator doesn't borrow it
    input.clear();

    let values: Vec<usize> = counter.collect();
    assert_eq!(values, vec![3, 4, 5, 6, 7]);
}

A small annotation that unlocks flexibility the compiler couldn’t infer on its own. If you’ve upgraded to Rust 2024 and hit mysterious “borrowed value does not live long enough” errors on impl Trait returns, use<> is likely the fix.

46. Let Chains — Flatten Nested if-let with &&

Deeply nested if let blocks are one of Rust’s most familiar awkward moments. Rust 1.88 (2024 edition) finally fixes it: let chains let you &&-chain multiple let bindings and boolean guards in one if or while.

Before let chains, matching several optional values forced you to nest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(name: Option<&str>, age: Option<u32>) -> String {
    if let Some(n) = name {
        if let Some(a) = age {
            if a >= 18 {
                return format!("{n} is an adult");
            }
        }
    }
    "unknown".to_string()
}

Three levels of indentation just to check two Options and a condition. The actual logic is buried at the bottom.

With let chains, it collapses to a single if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(name: Option<&str>, age: Option<u32>) -> String {
    if let Some(n) = name
        && let Some(a) = age
        && a >= 18
    {
        format!("{n} is an adult")
    } else {
        "unknown".to_string()
    }
}

Bindings introduced in earlier lets are in scope for all subsequent conditions — n is available when checking a, and both are available in the body.

The same syntax works in while loops. This example processes tokens until it hits one it can’t parse:

1
2
3
4
5
6
7
8
9
let mut tokens = vec!["42", "17", "bad", "99"];
tokens.reverse(); // process front-to-back

while let Some(token) = tokens.pop()
    && let Ok(n) = token.parse::<i32>()
{
    println!("{n}");
}
// prints: 42, 17  — stops at "bad"

Short-circuit semantics apply: if any condition in the chain fails, Rust skips the rest and takes the else branch (or ends the while loop).

To enable let chains, set edition = "2024" in your Cargo.toml:

1
2
[package]
edition = "2024"

No unstable flags, no feature gates — it’s stable as of Rust 1.88 and available on the 2024 edition. If you’re still on 2021, this alone is a good reason to upgrade.