Skip to content

← Errors Are Values step 23 of 24

Medium Primitives

unwrap and expect discipline

The starter function handles well-formed input, but on bad input it calls unwrap, expect or panic!. Each of those panics: it stops the current thread with an error message, and in a single-threaded program that usually ends the program. Your job is to keep the signature and report each failure as an Err value instead.

pub fn process(rows: Vec<String>) -> Result<Vec<i32>, String>

The exercise contract

Each row should have the form key=value. Process the rows in order:

  1. Split the row at its first =. If the row contains no =, it is bad.
  2. Trim whitespace from both ends of the key (with str::trim, which removes Unicode whitespace). If the trimmed key is empty, the row is bad.
  3. Trim the value the same way and parse it as an i32 with str::parse. That accepts ASCII digits with an optional leading + or -, from -2147483648 through 2147483647. Anything else is bad, including an empty value, 1.0, 1_000, non-ASCII digits and numbers outside the i32 range.

The checks run in that order, so a row such as =x reports the empty key, not the value. If every row is good, return Ok with the parsed values in input order. An empty input list is good and returns Ok(vec![]).

At the first bad row, return Err immediately with one of these messages and do not look at later rows:

problem example row exact error string
no = oops row `oops` has no `=`
key empty after trimming =5 row `=5` has an empty key
value does not parse as i32 a=x row `a=x` has a non-numeric value

In the messages, the row is copied exactly as it was given, before any trimming, between backticks. For example, the row " =1" produces row ` =1` has an empty key.

The gate: five Clippy lints that are off by default

Clippy is Rust's official collection of extra lints (warnings beyond the ones the compiler itself issues). The starter and the solution both begin with this attribute:

#![deny(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::unwrap_in_result,
    clippy::panic_in_result_fn
)]

All five lints belong to Clippy's restriction group, which is allowed (not reported) by default. Without this attribute the starter compiles and cargo clippy -- -D warnings reports nothing. Nothing in default Rust or Clippy forbids unwrap; a project has to opt in to lints like these.

#![deny(...)] turns each listed lint into an error when Clippy runs, so with the attribute the starter fails Clippy with five errors. Checked with Clippy 1.98, they are:

error: used `unwrap()` on an `Option` value
error: used `expect()` on a `Result` value
error: `panic` should not be present in production code
error: `expect` used in a function that returns a `Result`
error: used `panic!()` or assertion in a function that returns `Result`

The last two lints only apply inside a function whose return type is Result. In that situation the signature already tells callers that the function can fail, so the error belongs in the returned Result rather than in a panic. On this toolchain unwrap_in_result reports unwrap and expect calls on Result values, so here it flags the expect on the parse; the unwrap on split_once, which returns an Option, is reported by unwrap_used. panic_in_result_fn also reports assertion macros such as assert!.

Writing useful expect messages

These lints do not mean expect is always wrong. Sometimes a value really cannot be missing, for example when it was checked earlier or comes from a constant, and expect documents that assumption. Banning unwrap alone does not help much if the code just switches to .expect("failed").

The standard library documentation for Result::expect recommends phrasing the message as the condition you expected to hold, often using the word "should":

// Useful: each message names the assumption that turned out to be false.
.expect("hardcoded IP address should be valid")
.expect("cache should have been primed by init()")
.expect("config schema guarantees this key exists")

// Less useful: the panic location already says which call failed,
// and these messages add nothing about why it was expected to succeed.
.expect("failed to parse")
.expect("should work")
.unwrap()

A message written this way acts like a comment the compiler cannot check. If the assumption is ever false, the panic message tells the reader which assumption it was.

What the panic messages look like

Checked on Rust 1.98.1:

call on a failing value panic message
Option::unwrap() called `Option::unwrap()` on a `None` value
Option::expect(m) m
Result::unwrap() called `Result::unwrap()` on an `Err` value: followed by the error's Debug output
Result::expect(m) m: followed by the error's Debug output

For a Result, the error value is printed with its Debug formatting after your message, so a helpful Debug output on error types pays off here. This is also why r.ok().expect(m) gives a worse message than r.expect(m): .ok() converts the Result into an Option and discards the error, so only m is printed.

Related lints

  • panicking_unwrap is in Clippy's correctness group, which is denied by default. It reports an unwrap that will always panic, such as if o.is_none() { o.unwrap() }.
  • unnecessary_unwrap (warned by default) reports an unwrap after a check that already proved it succeeds, such as if o.is_some() { o.unwrap() }, where if let Some(x) = o is clearer.
  • get_unwrap (a restriction lint, off by default) reports v.get(1).unwrap(), which panics in the same cases as indexing with v[1].

None of these lints finds every possible panic. Other operations panic too: slice indexing out of bounds, integer division by zero, and arithmetic overflow when overflow checks are enabled (the default in debug builds; release builds wrap by default). Clippy has separate opt-in lints for some of these, such as indexing_slicing. Others are not reported by the lints above. For example, missing_panics_doc did not report a public function whose only possible panic was v[1]. Denying unwrap_used removes one source of panics; it does not make a program panic-free.

Notes

  • Because the split is at the first =, the row a=1=2 has key a and value 1=2, which does not parse, so it reports a non-numeric value.
  • Option::ok_or_else turns None into an Err, and Result::map_err replaces the error inside an Err. Both take a closure that runs only on the failure path, so the format! call that builds the message is skipped when the row is good.
  • The grade is: the code compiles, the tests pass, and cargo clippy -- -D warnings reports nothing. The #![deny(...)] attribute already makes these five lints errors on its own; -D warnings additionally turns every remaining warning into an error.