Attributes

#048 Mar 2026

48. #[expect] — Lint Suppression That Cleans Up After Itself

Silencing a lint with #[allow] is easy — but forgetting to remove it when the code changes is even easier. #[expect] suppresses a lint and warns you when it’s no longer needed.

The problem with #[allow]

You add #[allow(unused_variables)] during a refactor, the code ships, months pass, the variable gets used — and the stale #[allow] stays forever:

1
2
3
4
5
#[allow(unused_variables)]
let connection = db.connect(); // was unused during refactor
// ... later someone adds:
connection.execute("SELECT 1");
// the #[allow] above is now pointless, but no one notices

Over time, your codebase collects these like dust. They suppress diagnostics for problems that no longer exist.

Enter #[expect]

Stabilized in Rust 1.81, #[expect] works exactly like #[allow] — but fires a warning when the lint it suppresses is never triggered:

1
2
3
4
5
6
#[expect(unused_variables)]
let unused = "Suppressed — and the compiler knows why";

#[expect(unused_variables)] // ⚠️ warning: this expectation is unfulfilled
let used = "I'm actually used!";
println!("{used}");

The first #[expect] is satisfied — the variable is unused, and the lint stays quiet. The second one triggers unfulfilled_lint_expectations because used isn’t actually unused. The compiler tells you: this suppression has no reason to exist.

Add a reason for future you

#[expect] supports an optional reason parameter that shows up in the warning message:

1
2
3
4
#[expect(dead_code, reason = "will be used once the API module lands")]
fn prepare_response() -> Vec<u8> {
    vec![0x00, 0xFF]
}

When prepare_response gets called and the expectation becomes unfulfilled, the warning includes your reason — so future-you (or a teammate) knows exactly why it was there and that it’s safe to remove.

Works with Clippy too

#[expect] isn’t limited to compiler lints — it works with Clippy:

1
2
3
4
#[expect(clippy::needless_return)]
fn legacy_style() -> i32 {
    return 42;
}

This is perfect for migrating a codebase to stricter Clippy rules incrementally. Suppress violations with #[expect], fix them over time, and the compiler will tell you when each suppression can go.

#[allow] vs #[expect] — when to use which

Use #[allow] when the suppression is permanent and intentional — you never want the lint to fire here. Use #[expect] when the suppression is temporary or when you want a reminder to revisit it. Think of #[expect] as a // TODO that the compiler actually enforces.

38. #[must_use] — Never Ignore What Matters

Rust’s #[must_use] attribute turns silent bugs into compile-time warnings — making sure important return values never get accidentally ignored.

The Problem: Silently Ignoring Results

Here’s a classic bug that can haunt any codebase:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn remove_expired_tokens(tokens: &mut Vec<String>) -> usize {
    let before = tokens.len();
    tokens.retain(|t| !t.starts_with("exp_"));
    before - tokens.len()
}

fn main() {
    let mut tokens = vec![
        "exp_abc".to_string(),
        "valid_xyz".to_string(),
        "exp_def".to_string(),
    ];

    // Bug: we call the function but ignore the count!
    remove_expired_tokens(&mut tokens);

    // No warning, no error — the return value just vanishes
}

The function works fine, but the caller threw away useful information without even a whisper from the compiler.

The Fix: #[must_use]

Add #[must_use] to the function and the compiler has your back:

1
2
3
4
5
6
#[must_use = "returns the number of removed tokens"]
fn remove_expired_tokens(tokens: &mut Vec<String>) -> usize {
    let before = tokens.len();
    tokens.retain(|t| !t.starts_with("exp_"));
    before - tokens.len()
}

Now if someone calls remove_expired_tokens(&mut tokens); without using the result, the compiler emits:

1
2
3
4
warning: unused return value of `remove_expired_tokens` that must be used
  --> src/main.rs:14:5
   |
   = note: returns the number of removed tokens

Works on Types Too

#[must_use] isn’t just for functions — it shines on types:

1
2
3
4
5
#[must_use = "this Result may contain an error that should be handled"]
enum DatabaseResult<T> {
    Ok(T),
    Err(String),
}

This is exactly why calling .map() on an iterator without collecting produces a warning — Map is marked #[must_use] in std.

Already in the Standard Library

Rust’s standard library uses #[must_use] extensively. Result, Option, MutexGuard, and many iterator adapters are all marked with it. That’s why you get a warning for:

1
vec![1, 2, 3].iter().map(|x| x * 2);  // warning: unused `Map`

The iterator does nothing until consumed — and #[must_use] makes sure you don’t forget.

Quick Rules

Use #[must_use] when:

  • A function returns a Result or error indicator — callers should handle failures
  • A function is pure (no side effects) — ignoring the return means the call was pointless
  • A type is lazy (like iterators) — it does nothing until consumed
  • The return value carries critical information the caller likely needs

The custom message string is optional but highly recommended — it tells the developer why they shouldn’t ignore the value.