15-rustTermsLevel_07#[mustuse]

#[must_use]

Level 7 — Modules, Visibility & Project Structure Emits a compiler warning when a returned value is silently ignored — why Result and lazy iterators nag you if you drop them.


1. Prerequisites

  • Result<T, E> — The most famous type carrying this attribute in the standard library.
  • Lazy Evaluation — Why iterators are also marked #[must_use].
  • fn — What this attribute can also be applied directly to, on the function itself.

2. Term Category

Lint Attribute (the "don't just throw this away" warning): #[must_use] marks a type (or a specific function's return value) so that if calling code produces one and then discards it without ever reading, storing, or otherwise using it, the compiler emits a warning — flagging what's very often a genuine, silent bug.


3. Explanation

(1) Design Motivation — "Why did we design this?"

Consider some_operation_that_might_fail(); where the function returns a Result<T, E> but the call's return value is never bound to anything. Was the error silently ignored on purpose, or did the developer simply forget the ? or a .unwrap()/match? Without any signal, this is indistinguishable from a real bug — and in practice, it very often is one: an operation failed, and nobody noticed. #[must_use], applied to Result (and Option, and iterators, and several other types) in the standard library, makes the compiler flag exactly this situation with a warning, nudging the developer to explicitly handle or intentionally discard (via let _ = ...;) the value, rather than silently dropping it by accident. It's a lightweight, zero-runtime-cost static check that catches an entire class of "I forgot to check the result" bugs at compile time.

(2) Reality Metaphor

Imagine a package delivery service that requires an explicit signature confirming "I received this and either accepted or explicitly refused it," rather than allowing the courier to just leave a package on the doorstep and drive off with no acknowledgment.

  • Without #[must_use]: The courier drops a package (a Result) at the door and leaves immediately. Maybe the homeowner picked it up, maybe it sat there and got rained on and ruined — nobody can tell from the delivery log alone whether the outcome was actually addressed.
  • With #[must_use]: The delivery system flags an alert the moment a package is dropped without a corresponding signature — "package #4471 was delivered but never signed for!" It doesn't force the homeowner to do anything specific with it, but it makes silent, unacknowledged deliveries impossible to overlook.

(3) Rust Code Examples

Short Snippet (The Classic Result Warning)

fn risky_operation() -> Result<i32, String> {
    Err("something went wrong".to_string())
}

fn main() {
    risky_operation(); // WARNING: unused `Result` that must be used
    // The compiler flags this because Result<T, E> is #[must_use] in the standard library.

    // Explicitly acknowledging the discard silences the warning:
    let _ = risky_operation(); // "I'm intentionally ignoring this."
}

Fuller Example (Applying #[must_use] to Your Own Type)

#[must_use = "a Transaction does nothing until you call .commit() or .rollback()"]
struct Transaction {
    committed: bool,
}

impl Transaction {
    fn begin() -> Self { Transaction { committed: false } }
    fn commit(mut self) { self.committed = true; println!("committed!"); }
}

fn main() {
    Transaction::begin(); // WARNING: unused `Transaction` that must be used
                           // a Transaction does nothing until you call .commit() or .rollback()

    let tx = Transaction::begin();
    tx.commit(); // No warning — the value was actually used.
}

4. Common Mistakes & Pitfalls

Mistake 1: Reflexively Silencing Warnings with let _ = ...; Without Checking Errors

The mistake: Blindly writing let _ = risky_operation(); to silence #[must_use] warnings.

Why it is wrong: #[must_use] exists to warn developers of unhandled errors or side effects. Blindly discarding results via let _ = ... swallows errors without handling potential runtime panics or failures.

Incorrect:

let _ = std::fs::remove_file("config.json"); // ❌ Silently ignores deletion failures!

Fix:

if let Err(e) = std::fs::remove_file("config.json") {
    eprintln!("Failed to remove config file: {e}");
}

Mistake 2: Expecting #[must_use] to Produce a Compile Error Instead of a Warning

The mistake: Expecting #[must_use] to stop cargo build with a hard compile error.

Why it is wrong: #[must_use] emits a compiler warning (unused_must_use), not a hard error. To turn it into a hard error in CI, use #![deny(unused_must_use)] or RUSTFLAGS="-D unused_must_use".

Mistake 3: Forgetting custom error messages in #[must_use = "..."]

The mistake: Annotating custom guard types with plain #[must_use].

Why it is wrong: Omitting descriptive messages leaves callers wondering why the return value must be used. Adding #[must_use = "locks are released immediately if unassigned!"] clarifies proper API usage.


5. Practice Exercises

Exercise 1: Predict Whether a Warning Fires

Scenario: Does the following code trigger a #[must_use] warning? Why or why not?

fn get_value() -> Option<i32> { Some(42) }

fn main() {
    if get_value().is_some() {
        println!("got a value");
    }
}
Answer

No warning. Even though Option<T> is #[must_use], calling .is_some() on the returned Option counts as using it — the value was consumed by that method call, not silently dropped. The warning only fires when a #[must_use] value is produced and then discarded with no use at all, such as a bare get_value(); statement with the return value completely unreferenced.


Exercise 2: Annotating Custom Functions with #[must_use]

Scenario: Annotate #[must_use = "calculating area returns a value that should be used"] fn area(r: f64) -> f64.

Expected output:

Answer

Area: 78.53981633974483

Implementation

#[must_use = "calculating area returns a value that should be used"]
fn area(r: f64) -> f64 { std::f64::consts::PI * r * r }
fn main() {
    let a = area(5.0);
    println!("Area: {}", a);
}

Technical Explanation

#[must_use] triggers compiler warnings if returned values are unused.


Exercise 3: Annotating Custom Struct Types with #[must_use]

Scenario: Annotate #[must_use] struct Guard; so any unassigned instance generation triggers compiler warnings.

Expected output:

Answer

Guard assigned

Implementation

#[must_use]
struct Guard;
fn main() {
    let _g = Guard;
    println!("Guard assigned");
}

Technical Explanation

Annotating types with #[must_use] applies warnings to all functions returning instances of that type.


  • Result<T, E>
  • Lazy Evaluation — Why Iterator-returning adapter methods are also #[must_use]: an unused iterator chain silently does nothing at all.
  • ? Operator — The most common, idiomatic way to properly "use" a Result that would otherwise trigger this warning.

7. Key Takeaways

  • #[must_use] warns when a value of the marked type is produced and then completely discarded without ever being read or used.
  • It's applied to Result, Option, and iterator-returning methods in the standard library — precisely the cases where silently dropping the value is very often a genuine bug.
  • You can apply it to your own types (or specific functions) too, optionally with a custom message explaining why the value matters.
  • let _ = expr; explicitly and intentionally silences the warning — use it deliberately, not reflexively.
Built with LogoFlowershow