I recently started learning the Rust programming language by going through "the book", which does a phenomenal job of explaining the language basics.

After working through the book’s main content I got started with my first non-trivial, real-world application. But I soon found myself faced with a question I didn’t yet feel well-equipped to handle:

“How should you structure error handling in a mature rust application?"

This article describes my journey of discovering the answer to this question. I will try to explain the pattern I’ve settled on together with example code showing its implementation, in the hope that other newcomers may have an easier time getting started.

Contents

Intro

While the book goes through the basics of error handling, including use of the std::Result type and error propagation with the ? operator, it largely glosses over the different patterns for using these tools in real-world applications or the trade-offs involved with different approaches. 1

When I began looking into best practices, I came across quite a bit of outdated advice to use the failure crate. Failure had a semi-official feel to it as a result of being in the rust-lang-nursery namespace, but it has recently been deprecated.

There have been a number of improvements to the std::error::Error trait in the past two years. 2 These have made failure less needed in general and have sparked a number of more modern libraries taking advantage of these improvements to offer better ergonomics.

After reading through quite a lot of historical context and evaluating a number of libraries, I’ve now settled on a (largely library-agnostic) pattern for structuring errors, which I implement using the anyhow and thiserror crates. 3

The rest of this article will:

  1. Introduce a relatively trivial word-counting application to explore and explain the problem space.
  2. Explain why applications and libraries should use different error-handling patterns.
  3. Demonstrate how to apply these patterns using anyhow and thiserror.

Counting words

Let’s introduce some example code for use throughout the rest of this article. We’ll build a program to count the number of words in a text file, much like wc -w would do.

A naive implementation with basic error handling using std::Result might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

use std::env;use std::error::Error;use std::fs::File;use std::io::prelude::*;use std::io::BufReader;/// Count the number of words in the given input.
///
/// Any potential errors, such as being unable to read from the input will be propagated
/// upwards as-is due to the use of `line?` just before `split_whitespace()`.
fn count_words<R: Read>(input: &mut R) -> Result<u32, Box<dyn Error>> {    let reader = BufReader::new(input);    let mut wordcount = 0;    for line in reader.lines() {        for _word in line?.split_whitespace() {            wordcount += 1;        }    }    Ok(wordcount)}fn main() -> Result<(), Box<dyn Error>> {    for filename in env::args().skip(1).collect::<Vec<String>>() {        let mut reader = File::open(&filename)?;        let wordcount = count_words(&mut reader)?;        println!("{} {}", wordcount, filename);    }    Ok(())}

Let’s generate an input file for our new word counter and try to run it:

$ fortune > words.txt
$ cargo run --quiet -- words.txt
50 words.txt