Where Did Your Money Go?

Imagine a payment system. The user taps “Confirm Payment” in the app, the backend deducts the money, but the order status never updates. The user gets a “Payment Successful” notification, but the money vanishes into thin air.

The worst part of this kind of bug? The code runs without any errors. Everything looks normal on the surface, but the money is gone and so is the order.

In many cases, the culprit is a return value quietly ignored. Like this:

fn create_transaction(tx_args: TxArgs) -> Transaction {
    let id = generate_transaction_id();
    Transaction::new(id, tx_args)
}

// The caller:
create_transaction(tx_args); // compiles fine, silently drops at runtime

The caller forgot to capture the return value with let _ =, and the compiler says nothing. The business logic breaks somewhere in the middle, and now you’re debugging in production trying to figure out what went wrong.

#[must_use] solves exactly this. It’s a small label Rust gives you to attach to types or functions. Once attached, the compiler will warn you whenever someone ignores the return value.

Before and after #[must_use]: what changes when you add it


On a Struct: Every Return Path Gets Warned

The most straightforward approach is to put #[must_use] right on the struct definition. That way, any function returning this type will trigger a warning if the caller doesn’t use the value.

#[must_use = "Transaction must be processed after creation, not silently discarded"]
pub struct Transaction {
    id: u64,
    tx_args: TxArgs,
}

impl Transaction {
    pub fn new(id: u64, tx_args: TxArgs) -> Self {
        Self { id, tx_args }
    }
}

Now the compiler will warn at every spot where a Transaction is seen but not used:

create_transaction(args); // warning: unused `Transaction` that must be used

Transaction::new(1, args); // warning: unused `Transaction` that must be used

AnotherFunctionThatReturnsATransaction(); // warning: unused `Transaction` that must be used

Transaction { id: 1, tx_args: args }; // warning: struct literals count too

The warning includes your custom message, making it easier to track down:

warning: unused `Transaction` that must be used
  = note: Transaction must be processed after creation, not silently discarded

The caller is forced to make a choice: either actually use the value, or explicitly discard it:

let tx = create_transaction(tx_args);
// normal business logic

let _ = create_transaction(tx_args); // explicit discard, no warning

That let _ = pattern is actually good practice. It tells future readers: “this discard is intentional.”


On a Function: Targeted Control at Specific Call Sites

Putting #[must_use] on a struct is powerful, but sometimes you only want to constrain a specific function, not every path that returns that type.

In a payment system, you might have multiple ways to construct a transaction. Some are legitimate fast paths that don’t need the constraint:

// Only this function requires the caller to handle the return value
#[must_use = "Returned Transaction must be processed, not silently discarded"]
pub fn create_transaction(tx_args: TxArgs) -> Transaction {
    let id = generate_transaction_id();
    Transaction::new(id, tx_args)
}

// This is an internal utility — caller doesn't need to handle the return
fn build_empty_transaction() -> Transaction {
    Transaction::new(0, TxArgs::default())
}

The #[must_use] on create_transaction triggers a warning when its result is ignored. But Transaction::new() itself isn’t marked, so the call inside build_empty_transaction doesn’t fire any warning.

This fine-grained control fits API design well. You want users to go through your designated entry function, not bypass it and use the constructor directly.


On a Trait: Both impl Trait and dyn Trait Covered

The third option is to put #[must_use] on a trait definition itself. Now any function returning impl Trait or Box<dyn Trait> will warn if the caller ignores the result.

#[must_use = "Payment must be processed after creation"]
pub trait Payment {
    fn process(&self);
}

pub fn create_payment(amount: u64) -> impl Payment {
    CreditCardPayment { amount }
}

pub fn create_dynamic_payment(amount: u64) -> Box<dyn Payment> {
    Box::new(CreditCardPayment { amount })
}

Both return forms are covered:

create_payment(50);        // warning: unused implementer of `Payment` that must be used
create_dynamic_payment(50); // warning: unused boxed `Payment` trait object that must be used

let _ = create_payment(50); // explicit ignore, fine

This pattern shines for payments, notifications, writes — anything where skipping the result causes real problems. Adding the mark to the trait constrains every consumer of that abstraction.

How the compiler intercepts ignored return values


You’ve Been Using It All Along

#[must_use] is everywhere in the standard library. You’ve been benefiting from it without realizing it.

Here’s Result’s definition:

#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

This is why the compiler warns you when you ignore a Result: the attribute is baked into Result’s definition, so every function returning Result automatically gets this protection.

Option::is_some has a similar mark:

#[must_use = "if you intended to assert that this has a value, consider `.unwrap()` instead"]
pub const fn is_some(&self) -> bool {
    // ...
}

Pointing out that if you really mean to assert existence, .unwrap() is the clearer choice.


When Should You Use It?

A practical guide:

Mark operations that cannot be skipped: transactions, payments, writes, sending notifications.

Don’t mark pure data transformations, like String::from or Vec::new. Callers sometimes just want an intermediate result — data doesn’t have a “must handle” requirement.

Be conservative with infrastructure libraries. If it’s a public API or business logic layer, it’s worth adding. If it’s a low-level utility, overuse creates noise with let _ = everywhere.

There’s zero runtime cost. The check happens at compile time and vanishes afterward. It’s a small expression of Rust’s philosophy: make errors surface before the program runs.


FAQ

Q: I added #[must_use] but I’m not sure how to handle the return value yet. What do I do?

A: Start with let _ =. Get it compiling first, then figure out the business logic. At least you won’t miss anything. Come back and replace let _ = once you’ve thought it through.

Q: I put #[must_use] on both the struct and a function that returns it. Will the compiler warn twice?

A: No. It only warns once. And function-level marking is more granular — if your API design needs fine control, prefer marking the function.

Q: Can I mark just one variant of an enum with #[must_use]?

A: No, you can only mark the enum as a whole. You’d need to distinguish variants through logic, or extract the variant into its own type that carries the mark.