Skip to content

← Errors Are Values step 21 of 24

Medium Framework

#[must_use] in depth

Fix the annotations on a small builder API, then fix the code that misuses it.

pub fn build(ops: Vec<String>) -> Vec<String>

build starts from Config::default() (localhost:80) and runs a list of ops:

op effect pushes
"port:N" with_port(N)
"host:H" with_host(H)
"reset" back to the defaults
"show" "{host}:{port}"
"validate" "ok {host}:{port}" or "err {message}"
anything else "unknown"

Validation rejects an empty host ("host must not be empty", checked first) and any port below 1024 ("port 80 is privileged").

Three annotations, and two of them are wrong

The starter marks everything #[must_use], which is the mistake people make after being told the attribute is good. Clippy names both errors:

error: this function has a `#[must_use]` attribute with no message, but
       returns a type already marked as `#[must_use]`
   |
   |     #[must_use]
   |     pub fn validate(self) -> Result<Validated, String> {

clippy::double_must_use is on by default. Result is already #[must_use], so annotating the function adds nothing — it just makes the diagnostic worse, because the generic “unused return value” replaces Result‘s specific “this Result may be an Err variant”. If you want a better message, give the attribute one: #[must_use = "…"]. If you have nothing to add, leave it off.

error: this unit-returning function has a `#[must_use]` attribute
   |
   |     #[must_use]
   |     pub fn reset(&mut self) {

clippy::must_use_unit, also on by default. A function returning () has no value to use, so the attribute makes every call site a warning. Look at the starter’s compile output: unused return value of Config::reset that must be used fires on a perfectly ordinary cfg.reset();.

The one that stays is with_port, and it is worth being clear about why:

#[must_use]
pub fn with_port(self, port: u16) -> Self { … }

It takes self by value and returns a new Config. Ignoring the result does not modify anything — it silently discards the change. That is a bug the compiler can catch for you, and the starter’s build contains exactly that bug.

Contrast a &mut self setter — fn set_port(&mut self, port: u16) — which mutates in place and returns nothing. That one needs no annotation, and would be must_use_unit if you added one. The attribute belongs on the methods where “call it and ignore it” is meaningless.

Why the Validated newtype is annotated instead

#[must_use]
pub struct Validated(Config);

Three kinds of #[must_use] check exist, and this is the first:

  1. type-based — on a struct, enum, union or trait. Every expression of that type must be used. Result, Iterator and MustUse adaptors work this way.
  2. function-based — on a function or method. Its return value must be used.
  3. trait-based — on a trait, covering impl Trait return positions.

Marking the type is the stronger and more maintainable choice: it covers every function that ever produces one, including ones written years later. Marking the function is right when the type is generally fine to discard but this particular function’s result is not.

The Reference adds a rule that surprises people: #[must_use] has no effect on trait impl items. Annotating fn fmt inside an impl Display does nothing. Put it on the trait’s declaration instead.

What counts as “using” it

Verified on this toolchain:

f();            // warns
{ f() };        // warns  — the lint looks THROUGH a block
Some(f());      // silent — wrapping counts as a use
(f(),);         // silent — same
let _ = f();    // silent — the sanctioned opt-out
_ = f();        // silent — same, since 1.59

The two silent middle cases are worth noticing, because they are how a value escapes the check without anyone meaning it to. They are not loopholes to reach for — let _ = is the one that says “deliberate” to a reviewer.

Notes

  • Under -D warnings, all of this has teeth: unused_must_use is in rustc’s unused group, so an ignored Result is a build failure, not a nag.
  • must_use_candidate (pedantic, off here) suggests adding the attribute to every pure public function that returns something. It is a lot of noise; most crates leave it off and annotate deliberately.
  • return_self_not_must_use (pedantic) is the specific case this problem is built on: a method returning Self almost always wants #[must_use].
  • validate consumes self, so build clones before calling it — otherwise the config could not be shown afterwards.

Remember the grade is compile + tests + clippy -D warnings.