Skip to content

← Errors Are Values step 7 of 24

Easy Primitives

Result, and why it is #[must_use]

Evaluate a list of division ops, each written "a/b", and return one string per op.

pub fn checked_div(a: i32, b: i32) -> Result<i32, String>
pub fn apply(ops: Vec<String>) -> Vec<String>
  • a well-formed op that divides cleanly → the quotient, e.g. "10/2""5"
  • a zero divisor → "error: division by zero"
  • a slash with a non-number on either side → "error: not a number"
  • no slash at all → "error: malformed"

A failing op must not stop the ones after it, and both sides are trimmed, so " 12 / 4 " is "3".

The starter compiles. Then the gate deletes it.

Two things are wrong with it, and they are the two things this problem exists to teach.

1. It ignores a Result

warning: unused `Result` that must be used
   |
   |         checked_div(a, b);
   |         ^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
help: use `let _ = ...` to ignore the resulting value

Result is declared #[must_use] in the standard library, so throwing one away is a warning. And the harness compiles with -D warnings, which promotes every warning to an error. unused_must_use lives in rustc’s unused group, so under this gate it is fatal. Your submission does not merely look sloppy — it does not build.

If you come from a language where ignoring a return value is free, this is the adjustment. In Java a checked exception forces the caller to write a catch that most people fill with a comment. In Go the compiler is happy to let err go unread. In Rust the fallibility is in the type, and a type you never consume is a value you dropped on the floor — so the same machinery that notices an unused Vec notices an unused Result.

There is a legitimate opt-out, and it is deliberately visible:

let _ = checked_div(a, b);   // "yes, I really mean to ignore this"

r.ok(); is not an opt-out — it produces an Option you also ignore, and clippy’s unused_result_ok will say so. Being explicit is the point: a reviewer can grep for let _ =.

2. Its error type is ()

error: this returns a `Result<_, ()>`
   |
   | pub fn checked_div(a: i32, b: i32) -> Result<i32, ()> {
   |                                       ^^^^^^^^^^^^^^^
   = help: use a custom `Error` type instead

clippy::result_unit_err is on by default for public functions, and its point is a design one, not a style one. Result<T, ()> says “this can fail, and I will tell you nothing about why”. That is exactly the information content of Option<T> — which is shorter, better supported, and does not mislead the reader into thinking an error is coming.

If the error carries no information, you wanted Option. If it does, give it a type.

Here, the error does carry information, so String will do for now. (A String error is scaffolding — Track 6 replaces it with a real error type later. Do not conclude that stringly-typed errors are idiomatic Rust.)

What Result actually is

Nothing magical — an enum, exactly like Option, with a second payload:

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

There is no unwinding, no stack of handlers, no special control flow. Ok and Err are ordinary variants; match, if let and let-else work on them the way they work on Some and None. The reference solution here uses let-else twice, because “bail out of this iteration on the boring case” is exactly what let-else is for.

Notes

  • The #[must_use] on Result is on the type, so it applies wherever a Result value is discarded, no matter which function produced it.
  • One documented exception: Result<(), E> where E is an uninhabited type (a type with no values) does not fire, because it cannot be an Err. A footnote, not a loophole.
  • let_underscore_must_use and double_must_use are the neighbouring lints; you will meet them properly later in this track.
  • Integer division in Rust truncates towards zero, so -9 / 2 is -4, not -5.

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