Skip to content
Lesson 5 of 9

Step 1 of 7 · Reading · ~3 min

Learn

Iterators, Result, Option

Result and Option: Custom Errors and ?

After this lesson you can give a program its own error type, let ? convert library errors into it, and shorten Option and Result pipelines.

In Rust Fundamentals a fallible function returned Result<T, String> and ? passed the error up. A String has no structure: callers can only compare text. Here the error is a type of its own, and ? converts between error types.

An error type is an enum

One variant per kind of failure, each carrying the data its message needs. Three impls finish it: Display is the message for a person, std::error::Error marks the type as an error (needs Debug and Display), and From<ParseIntError> tells ? how to turn the library's error into yours.

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum VolumeError {
    NoEquals(String),
    BadLevel(ParseIntError),
    TooLoud(i32),
}

impl fmt::Display for VolumeError {
    fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
        match self {
            VolumeError::NoEquals(s) => write!(out, "no '=' in {:?}", s),
            VolumeError::BadLevel(e) => write!(out, "bad level: {}", e),
            VolumeError::TooLoud(n) => write!(out, "{} is above 11", n),
        }
    }
}

impl std::error::Error for VolumeError {}

impl From<ParseIntError> for VolumeError {
    fn from(e: ParseIntError) -> Self {
        VolumeError::BadLevel(e)
    }
}

? converts through From

On Err(e), ? returns Err(From::from(e)), so a function returning Result<_, VolumeError> can use ? on a ParseIntError:

fn parse_level(text: &str) -> Result<i32, VolumeError> {
    let at = text.find('=').ok_or_else(|| VolumeError::NoEquals(text.to_string()))?;
    let level = text[at + 1..].parse::<i32>()?;   // ParseIntError becomes VolumeError
    if level > 11 {
        return Err(VolumeError::TooLoud(level));
    }
    Ok(level)
}
callresult
parse_level("vol=7")Ok(7)
parse_level("vol=x")bad level: invalid digit found in string
parse_level("vol=40")40 is above 11
parse_level("vol 7")no '=' in "vol 7"

Without the From impl the parse line fails:

error[E0277]: `?` couldn't convert the error to `VolumeError`
  the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `VolumeError`

Combinators

  • ok_or(err) turns None into Err(err); ok_or_else(|| err) builds the error only when needed (useful when it allocates, like to_string).
  • map(f) changes the success value. and_then(f) is for an f that itself returns an Option or Result; map would nest them: Some(4i32).map(|n| n.checked_mul(1_000_000_000)) is Some(None), and_then gives None.
  • filter(pred) turns a Some into None when pred is false: Some('7').filter(|c| c.is_alphabetic()) is None.
  • In a function returning Option, let mid = name.split_whitespace().nth(1)?; returns None early when there is no second word. You cannot mix: ? on an Option inside a function returning Result does not compile; convert with ok_or first.

Stopping at the first failure

An iterator of Results collects into one Result; the annotation picks that over a Vec of results.

let good: Result<Vec<i32>, ParseIntError> = "4 8 15".split(' ').map(|s| s.parse()).collect();
// Ok([4, 8, 15])
let bad: Result<Vec<i32>, ParseIntError> = "4 x 15 y".split(' ').map(|s| s.parse()).collect();
// Err(InvalidDigit): stops at "x"; "y" is never parsed

A specific enum lets callers match on the cause; the catch-all alternative is covered in Rust Advanced.

Common mistakes

  • Collapsing every parse failure into one message with map_err(|_| ...): x and 99999999999 print the same text. Wrapping the original error keeps std's message.
  • Plain +, -, * on i32. The grader builds without optimisations, so 2147483647 + 1 panics and later output is lost. Use checked_add and friends.
  • Guarding only division by zero. checked_div also returns None for i32::MIN / -1: that is overflow.
  • Splitting on ' '. add 3 4 (two spaces) has an empty token and prints bad number: cannot parse integer from empty string.

Your exercise

Build a line calculator around your own CalcError: the enum, its Display, Error and From<ParseIntError> impls, then next_number and eval using ?. The starter reads the lines and counts failures. The likeliest slip is unchecked arithmetic, which panics on overflow.

Up nextBox, Rc and RefCellSmart Pointers and Memory

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…