How to Use #[must_use] for Critical Return Values

Use the #[must_use] attribute on functions to trigger compiler warnings when their return values are ignored.

The silent drop problem

You write a function that parses a configuration file. It returns a Result<Config, ParseError>. You call it in main, but you forget to handle the Err case. The program compiles. It runs. It crashes three lines later when it tries to read a missing field. Rust could have caught this. It just needs a nudge.

#[must_use] is that nudge. It tells the compiler to flag any call site that ignores the return value. It does not stop compilation by default. It raises a warning. Think of it like a mandatory acknowledgment checkbox on a safety form. The form still submits if you skip it, but the system flashes a bright yellow alert until you check the box or explicitly mark it as handled.

Add the attribute. Let the compiler do the watching.

How the attribute works

Rust tracks expressions at compile time. Every function call is an expression that produces a value. When you write a function call as a standalone statement, Rust automatically inserts a semicolon and drops the value at the end of the statement. Most of the time, this is fine. Functions that return () or perform side effects do not need their return values.

Functions that return data, resources, or fallible results are different. Dropping their return value usually means a bug. #[must_use] attaches a compile-time contract to the function signature. The compiler checks how the expression is used. If the expression is discarded, the compiler emits an unused_must_use warning. The warning points directly to the call site.

You can attach the attribute to functions, structs, enums, and enum variants. When attached to a type, the warning triggers whenever a value of that type is created and immediately dropped. This covers constructors, factory methods, and any function that returns the type.

Bind the value or discard it explicitly. The compiler will not guess your intent.

Minimal example

/// Returns a freshly allocated string that the caller must own.
#[must_use]
fn create_greeting() -> String {
    // The String is allocated on the heap.
    // The caller takes ownership of the allocation.
    String::from("Hello, Rustacean")
}

fn main() {
    // The compiler warns here because the String is dropped immediately.
    // The allocation leaks until the process exits.
    create_greeting();
}

The warning tells you exactly what happened. It shows the call site and explains that the return value was not used. You can silence it by binding the value to a variable, passing it to another function, or explicitly discarding it with let _ = create_greeting();. The let _ = pattern is a community convention. It signals to human readers that you considered the return value and deliberately chose to ignore it. The compiler sees the binding and stops warning.

Walking through the compiler check

When rustc compiles the example, it builds an abstract syntax tree. It walks the tree and evaluates expression types. It sees create_greeting() and resolves the return type to String. It checks the function metadata and finds the #[must_use] attribute. It then checks the usage context.

The expression appears as a statement. Statements in Rust are expressions followed by a semicolon. The semicolon tells the compiler to evaluate the expression and discard the result. The compiler matches this pattern against the #[must_use] flag. The match triggers the unused_must_use lint. The lint is allowed by default, which means it produces a warning instead of an error.

You can customize the warning message. The attribute accepts an optional string literal.

/// Returns a database connection handle that must be closed.
#[must_use = "Connections leak file descriptors. Close them or store them."]
fn open_connection() -> String {
    String::from("conn://localhost:5432")
}

fn main() {
    // The custom message appears in the warning output.
    open_connection();
}

Custom messages are useful when the default warning lacks context. The default message says something like unused return value of create_greeting that must be used. The custom message explains the consequence. It reduces the time spent debugging silent failures.

Realistic patterns and type-level usage

Most real code uses #[must_use] on functions that return Result or Option. The standard library applies it to almost every function that can fail. You can apply it to your own types too. This is common in domain models where a value represents a fallible operation or a resource that requires cleanup.

/// Represents a fallible database query result.
#[must_use = "Queries can fail. Handle the error or panic intentionally."]
struct QueryResult {
    success: bool,
    data: Option<Vec<u8>>,
}

/// Executes a query and returns the result wrapper.
fn run_query() -> QueryResult {
    // Simulates a successful query.
    // The caller must decide what to do with the wrapper.
    QueryResult { success: true, data: Some(vec![1, 2, 3]) }
}

fn main() {
    // Warning triggers because the custom message is more descriptive.
    // The compiler knows the type carries a must_use contract.
    run_query();
}

The attribute works on traits as well. If you mark a trait method with #[must_use], every implementation inherits the contract. This is how the standard library ensures that Iterator::next() and TryInto::try_into() cannot be silently ignored. The compiler checks the trait definition, not just the concrete implementation.

Builder patterns rely heavily on #[must_use]. Builder methods return Self so you can chain calls. If a builder method is marked #[must_use], the compiler warns when you call it but forget to chain the next step or call build(). This prevents half-configured objects from leaking into your codebase.

Turn warnings into errors in libraries. Silent failures are worse than broken builds.

Pitfalls and escalation

#[must_use] is a warning, not a hard error. In production builds, warnings often get buried in CI logs. If ignoring the value is actually a bug, you need to escalate it. Add #![deny(unused_must_use)] at the crate root. This flips the warning into a compilation error. The build fails until every #[must_use] return value is handled.

Be careful with the crate-level deny flag. It applies to the entire crate, including dependencies. Some third-party code might trigger it. You can scope the deny to specific modules with #[deny(unused_must_use)] instead of the crate-level ! prefix. This gives you granular control. You can keep the warning in your application code and enforce the error in your library code.

Another pitfall is applying the attribute to functions that return (). The compiler ignores #[must_use] on unit-returning functions because there is nothing to ignore. It also does not work on main or #[test] functions in the way you expect. The test harness and runtime handle their return values automatically. The attribute is meant for library functions and domain logic.

Do not use #[must_use] to replace proper error handling. It is a compile-time guardrail that catches copy-paste mistakes and forgotten bindings. It does not fix architectural flaws. It does not replace ? operators or explicit match statements. It only ensures you acknowledge the value.

Mark the contract. Trust the compiler to enforce it.

When to apply it

Use #[must_use] on functions that return Result or Option when ignoring the value indicates a logic error. Use #[must_use] on functions that allocate heap memory or acquire system resources, so callers cannot accidentally leak them. Use a custom #[must_use = "message"] when the default warning lacks context about what goes wrong. Skip #[must_use] on logging functions, debug helpers, or side-effect-only methods that return (). Reach for #![deny(unused_must_use)] in library crates where silent failures would break downstream users.

The Rust community treats #[must_use] as a contract. If a function can fail, it gets the attribute. If a builder method returns Self, it gets the attribute. You will see it on Vec::new(), File::open(), and String::from(). It is idiomatic, expected, and rarely controversial. Apply it consistently. Your future self will thank you when a missing ? operator gets caught at compile time instead of in production.

Where to go next