Rust Error Tutorial

Best practices for handling errors in Rust

Benjamin Kay
crash dummy

Errare humanum est, perseverare autem diabolicum. Any non-trivial program will encounter conditions that produce an error. Any program not written to anticipate these errors is diabolical. Prima facie Rust appears to make error handling easy with the Result type, and The Rust Book does an excellent job explaining how to use it. Perhaps from overthinking things, or perhaps from too much experience with C++, I nevertheless became confused about the best practices for handling errors in real-world code. Soon I was longing for the relative simplicity of C++ exceptions and wondering how Rust, a language billed as being safer to use than C++, could lack such fundamental error handling constructs.

Fortunately, it turns out that Rust has very powerful, flexible error handling capabilities, and the real problem is that no one can agree on the "best" way to use them. Look no further than the profundity of error crates that have popped up for Rust. The sheer amount of choice is confusing for a novice in the language! In this tutorial, rather than develop yet another error handling crate, I will explain the fundamentals of Rust error handling and develop some best practices. After reading this tutorial you will be able to handle errors gracefully using just the Rust standard library. You will be able to judge for yourself which error handling crate best suits your coding style, should you choose to use one of them.

Table of Contents

  1. Result?
  2. Don't Panic!
  3. dyn Error
  4. Downcasting
  5. Custom Errors
  6. Chaining
  7. enum Error
  8. Conclusion

Result?

Let's begin with a variation on the standard hello world program. Instead of writing hello world to the standard output (i.e. terminal) we will write it to a file hello.txt. There are two steps that could go wrong here:

  1. We can't open the file for writing, perhaps because we don't have sufficient filesystem permissions.
  2. We can't write to the file, perhaps because the hard disk is full.
This simple example introduces the question mark operator (formerly the try!() macro) and demonstrates correct, idiomatic error propagation in Rust. We define a function hello() that performs the actual logic of creating and writing to hello.txt and is called by main(). The real action happens at line 5 where we call the write!() macro to actually write "Hello world!" to the file. The write!() macro returns a Result that is Ok(()) if the writing was succesful and Err(std::io::Error) if there was a problem. The Result is returned to main() where, good or bad, we will have to deal with it. (Recall that if we omit the semicolon a function returns the value of its last line.)
use std::io::Write; // needed for write!()

fn hello(filename: &std::path::Path) -> Result<(), std::io::Error> {
	let file = std::fs::File::create(filename)?; // the question mark
	write!(&file, "Hello world!\n")
}

fn main() -> Result<(), std::io::Error> {
	hello(std::path::Path::new("hello.txt"))
}
There are two bits of magic happening here. First, what happens in main() if the call to hello() returns an Err(std::io::Error)? It turns out if you write the main function to return a Result<_, Error> then it will automatically return success to the operating system on success, and if an error happens it will return failure and print out the error to the standard error terminal, stderr. It's a super-convenient way to have your program fail gracefully and propagate the error up to the operating system.

The second bit of magic is the question mark ? on line 4. A call to File::create() could fail and thus returns a Result<File, std::io::Error>. That's a problem because on the next line write!() expects a File, not a Result. (Try removing the question mark and the Rust compiler will give you an error.) In Rust parlance we need to "unwrap" the Result of File::create() before we can use it. The good news is this is not difficult to do, and the question mark is just syntactic sugar for this:

  let file = match std::fs::File::create(filename) {
		Ok(f) => f,
		Err(e) => return Err(e)
	};
The question mark operator is just shorthand for a match. On success the match "unwraps" the Ok(f) to the enclosed f. On failure the match ends the function hello() early by returning the Err(e).

This may not seem like a big deal, but consider this slightly more complicated example in which we copy the contents of one file to another one line at a time. Although it's a simple task, there's a lot that can go wrong. Can you count all the uses of the question mark operator?

use std::io::BufRead; // needed for lines()
use std::io::Write; // needed for writeln!() 

fn copy(infile: &std::path::Path, outfile: &std::path::Path)
	-> Result<(), std::io::Error> {
	let infile = std::fs::File::open(infile)?;
	let infile = std::io::BufReader::new(infile);
	let outfile = std::fs::File::create(outfile)?;
	for line in infile.lines() {
		writeln!(&outfile, "{}", line?)?;
	}
	Ok(())
}

fn main() -> Result<(), std::io::Error> {
	copy(std::path::Path::new("foo"), std::path::Path::new("bar"))
}
This example also serves to demonstrate another idiom: returning Ok(()) from a function. In the first example the last statement in hello() is the write!() macro, which just happens to return the needed Result. In this second example the function ends with a for loop, which in Rust always returns (). Since () is not a Result we have to wrap it in one by returning Ok(()) to signify successful completion of the function

In summary, the question mark operator ? is the idiomatic way of unwrapping a Result in rust. It is just syntactic sugar for a match that ends the function early by returning an error if one has occurred. However, as we saw in the second example, that syntactic sugar makes our code much more readable and easy to write! We also learned that the main() function can itself return a Result to the operating system. Now that we've learned the correct way to handle errors, in the next section we'll learn about the wrong way to handle them.

Don't Panic

Let's revisit the first example from the previous section in which we write "Hello world!" to a file hello.txt. We can encounter an error when we open the file, and we can encounter an error again when we try to write to it. We could have (incorrectly) written the program like this:

use std::io::Write; // needed for write!()

fn hello(filename: &std::path::Path) {
	let file = match std::fs::File::create(filename).unwrap(); {
	write!(&file, "Hello world!\n").expect("Error writing to file.");
}

fn main() {
	hello(std::path::Path::new("hello.txt"));
}
The above code compiles, but it's not a good way to handle errors. As you'll soon learn you should avoid using unwrap() or expect(). Let's desugar the above code to see what these functions do:
fn hello(filename: &std::path::Path) {
	let file = match std::fs::File::create(filename) {
		Ok(f) => f,
		Err(_) => panic!()
	};
	if let Err(_) = write!(&file, "Hello world!\n") {
		panic!("Error writing to file.");
	}
}
Aha! unwrap() desugars to a panic!(), something that (as the title of this section implies) you should not do. expect() also desugars to a panic!() but prints out an error message too.

What's wrong with panicking? When your program panics it terminates immediately, which isn't always necessary. In this example, if there is an error opening filename for writing, rather than panicking we could have prompted the use to enter a different file name. It would have been nice to give the author of main() or the progammer making use of our function/library that choice rather than making his program crash with a panic!().

There is an even more insidious reason not to panic. Look closely at the above example. Do you see a call to file.close()? There isn't one, and in fact if you look at the documentation for std::fs::File there is no close() function! How can this be? Objects in Rust that implement the Drop trait can define a block of code that is executed when the object is destroyed, or "dropped." The Drop trait for File closes the file. At the end of hello() the function's stack is unwound and the file variable that lives on its stack gets dropped, at which point its Drop trait is invoked and the file is automatically closed. It's very ergonomic: you don't have to remember to call a close() function, and you can't forget to close a file by accident! Programming nerds call this paradigm RAII: resource allocation is initialziation.

Remember how I said panic!() terminates the program immediately? Depending on how your program is compiled, it could be immediately! In one scenario, the hello() function never actually returns as it would have if we had used the question mark operator. Since it does not return, its stack is not unwound. The variables on its stack are not dropped. Their Drop traits are not invoked. And the file is not closed. In this example it's not a big deal because the operating system will automatically close all open file descriptors when the program terminates. However, imagine a more complex resource such as a connection to a server where the Drop trait transmits a goodbye message to the server. The operating system can't possibly know how to do that automatically, so if you panic those resources might not get cleaned up!

(As an aside, careful readers have pointed out that panicking actually does perform stack unwinding by default, most of the time. See panic::catch_unwind. Sometimes it may not, in particular when using panic=abort, which will typically be seen on embedded systems where memory/performance are needed. There can also be issues with propagating panics properly across threads or across foreign function interfaces. Bottom line, if you want your code to unwind after an error then your best bet is not to panic in the first place.)

There are some instances in which it is acceptable to panic. If an error is irrecoverable, or if trying to unwind after an error risks data corruption, panicking and terminating the program immediately may be the best option. For example, in C/C++ accessing memory past the end of an array will (if you're lucky) generate a segmentation fault and the program will terminate immediately without unwinding. Thanks to Rusts's memory safety you will never be able to access memory beyond the end of an array -- your program will reliably panic every time you try. Why not return an error? The rationale is that accessing memory beyond the end of an array should never happen if a program is written correctly. It's not the result of user error or a faulty network connection -- it's a result of programmer error, a bug.

In summary, if you expect an error to arise some of the time your program is executed (e.g. insufficient permission to create a file) then handle it by returning a Result, typically using the question mark ? operator. If an error should never occur, but might arise because the programmer (you) made a mistake, it is reasonable to panic. However, be warned that once you panic the program cannot recover and, moreover, it will not unwind or clean up resources before terminating.

dyn Error

As we saw in the first section, using the ? operator and returning a Result to propagate errors is ergonomic and the preferred way of dealing with errors in Rust. Let's look at a more complicated example of summing some newline-delimited numbers in a file. If the input file is:

1
2
3
4
Then the output of our program should be 1 + 2 + 3 + 4 = 10. We'll write a function count() that takes a file name, reads the file line by line, parses each line to a number, and returns the sum of those numbers. The inside of our function count() will look something like this:
let file = std::fs::File::open(filename)?;
let file = std::io::BufReader::new(file);
let mut sum: i32 = 0;
for line in file.lines() {
	sum += line?.parse::<i32>()?;
}
Ok(sum)

We can naively declare our function as we did in prior sections as fn count(filename: &std::path::Path) -> Result<i32, std::io::Error>. The compiler tells us:

error[E0277]: `?` couldn't convert the error to `std::io::Error`
  --> src/bin/count.rs:17:30
   |
   |   sum += line?.parse::<i32>()?;
   |                              ^
   |   the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `std::io::Error`
   |
What happened? In prior examples all of our errors were of type std::io::Error. In this example when we convert a line from the file from a string to an integer we get a Result that might contain a std::num::ParseIntError. The compiler is complaining that our function returns a Result with a std::io::Error when this ? operator could return a std::num::ParseIntError. Unfortunately we're really in a bind! If we change our function signature to return a Result with a std::num::ParseIntError the compiler will complain that our other ? operators could return a std::io::Error.

We need a way to write a function signature that follows the rules of the ? operator and can return more than one error type. One way to do this is to return the error as a trait object, a Box<dyn Error>. Here the dyn signifies the use of a so called "fat pointer" for runtime, dynamic typing. It means we can return a boxed (i.e. heap-allocated) object of any type that satisfies the std::error::Error trait. Here is the completed example using this strategy:

use std::io::BufRead; // needed for lines()

fn count(filename: &std::path::Path) -> Result<i32, std::boxed::Box<dyn std::error::Error>> {
	let file = std::fs::File::open(filename)?;
	let file = std::io::BufReader::new(file);
	let mut sum: i32 = 0;
	for line in file.lines() {
		sum += line?.parse::<i32>()?;
	}
	Ok(sum)
}

fn main() -> Result<(), std::boxed::Box<dyn std::error::Error>> {
	let sum = count(std::path::Path::new("numbers.txt"))?;
	println!("The sum is: {}", sum);
	Ok(())
}

There is one more caveat, which is that the Error trait does not require its implementations to be thread safe. Since Rust is designed to be a multithreaded language, it would be a big problem if we could not propagate errors between threads! Even if our function returns an error object that is thread safe, once that statically-typed object is converted into a dynamically-typed trait object, the compiler no longer knows whether or not it is thread safe. In essence, if we write our function signatures as they are written above, we "infect" all calling code to be non-thread-safe. Therefore the best practice for propagating errors using error traits is to also make them Send and Sync by writing our function signature like this:

fn count(filename: &std::path::Path)
	-> Result<
		i32,
		std::boxed::Box<dyn
			std::error::Error
			+ std::marker::Send
			+ std::marker::Sync
		>
	>

All those type contraints get to be verbose, so you may find it convenient to define a type alias BoxError as in the example below. In fact, this idiom is used so often that there is an RFC to create the BoxError type. Here is the completed example again using thread-friendly error trait objects:

use std::io::BufRead; // needed for lines()

pub type BoxError = std::boxed::Box<dyn
	std::error::Error   // must implement Error to satisfy ?
	+ std::marker::Send // needed for threads
	+ std::marker::Sync // needed for threads
>;

fn count(filename: &std::path::Path) -> Result<i32, BoxError> {
	let file = std::fs::File::open(filename)?;
	let file = std::io::BufReader::new(file);
	let mut sum: i32 = 0;
	for line in file.lines() {
		sum += line?.parse::<i32>()?;
	}
	Ok(sum)
}

fn main() -> Result<(), BoxError> {
	let sum = count(std::path::Path::new("numbers.txt"))?;
	println!("The sum is: {}", sum);
	Ok(())
}

In summary, if your function can encounter more than one type of Error then the easiest way to propagate those errors using the question mark operator is to return a boxed error trait. Rather than simply returning Box<dyn Error> (as you will see in many examples), you should declare the BoxError type alias (see the example above) and return a Result<_, BoxError>. Using this BoxError type will make your code behave better in a multithreaded environment.

Downcasting

In the preceding sections we have focused on how to propagate an error. Let's return to our example from the first section and think about how to handle and possibly recover from an error. If a function returns Err(std::io::Error) what we can we do with that? Let's look at the documentation for std::io::Error to find out. It looks like this type implements the Display and Debug traits. In fact, if we look at the documentation for the Error trait, we see that all errors have to implement these traits. That means we could write code like the following:

use std::io::Write; // needed for writeln!()
use std::io::BufRead; // needed for read_line()

// our original function that writes "Hello world!" to a file
fn hello(filename: &std::path::Path) -> Result<(), std::io::Error> {
	let file = std::fs::File::create(filename)?;
	writeln!(&file, "Hello world!")
}

fn main() -> Result<(), std::io::Error> {
	let mut path = std::string::String::new();
	while let Err(e) = {
		print!("Enter a path to a file: "); // prompt user for input
		std::io::stdout().flush()?;
		std::io::stdin().lock().read_line(&mut path)?; // read input
		let newline: &[_] = &['\r', '\n']; // trim trailing newline
		hello(std::path::Path::new(path.trim_end_matches(newline)))
	} {
		// if hello() returned an error then notify user and try again
		eprintln!("An error occured writing to: {}Details: {}", path, e);
	}
	Ok(())
}
$ cargo run
Enter a path to a file: /root/foo
An error occured writing to: /root/foo
Details: Permission denied (os error 13)
Enter a path to a file:
The while let Err(e) matches the last line of the while statement, which is our call to hello(). If an error occurs then the following block containing eprintn!() is executed to print an error message to the standard error terminal, stderr. Then the while loop repeats, prompting the user to try entering another file path. If hello() returns Ok(()) then the while loop terminates and the program ends.

We can get even more sophisticated by customizing our error message. Again looking at std::io::Error we discover kind() and raw_os_error() functions that allow us to reformat the error message in the following tongue-and-cheek example:

		// if hello() returned an error then notify user and try again
		eprint!("Good sir, I regret to inform you that I could not write to: {}", path);
		eprintln!("Indubitably, I did encounter an error of type {:?}.", e.kind());
		if let Some(code) = e.raw_os_error() {
			eprintln!("The secret error code was numbered: {}", code);
		}
$ cargo run
Enter a path to a file: /root/foo
Good sir, I regret to inform you that I could not write to: /root/foo
Indubitably, I did encounter an error of type PermissionDenied.
The secret error code was numbered: 13
Enter a path to a file:
What fun! More seriously, we could programmatically match on ErrorKind or the integer value of raw_os_error() to make our program react differently to specific input/output errors, for instance, handling a PermissionDenied error differently from a WriteZero error.

Let's get more advanced and try to develop our counting example from the dyn Error section.

pub type BoxError = std::boxed::Box<dyn
	std::error::Error   // must implement Error to satisfy ?
	+ std::marker::Send // needed for threads
	+ std::marker::Sync // needed for threads
>;

use std::io::BufRead; // needed for lines()

fn count(filename: &std::path::Path) -> Result<i32, BoxError> {
	let file = std::fs::File::open(filename)?;
	let file = std::io::BufReader::new(file);
	let mut sum: i32 = 0;
	for line in file.lines() {
		sum += line?.parse::<i32>()?;
	}
	Ok(sum)
}

fn main() -> Result<(), BoxError> {
	match count(std::path::Path::new("numbers.txt")) {
		Ok(sum) => {
			println!("The sum is: {}", sum);
			Ok(())
		}
		Err(e) => {
			eprintln!("An error occured: {}", e);
			Err(e)
		}
	}
}
So far so good. We can match on the Result of the count() function and print out the sum of the lines in the file if it worked, or else print out the error message. This works because the Error trait implements the Display trait, so we can eprintln!("{}", e) any let e: BoxError.

What if we want to get fancy and reformat the error message, or to programatically handle different kinds of std::io::Error differently? Naively we can try:

		Err(e) => {
			eprintln!("An error occured of kind: {:?}", e.kind());
			Err(e)
		}
   |
   |             eprintln!("An error occured of kind: {:?}", e.kind());
   |                                                           ^^^^
   | method not found in `std::boxed::Box<dyn std::error::Error + std::marker::Send + std::marker::Sync>`
Uh-oh, what happened here? The Rust compiler is telling us that the error e doesn't have a method kind(). But we know from the prior example that std::io::Error does have a method kind(), so what gives here? Our method count() can actually return more than one error type through the error trait object BoxError. The error might be a std::io::Error, but it might also be a std::num::ParseIntError, the latter of which does not have a kind() method. We won't know which type of error (if any) we encountered until runtime, so there's no way for the compiler to know if e has a kind() method or not; it will conservatively assume not and generate a compilation error.

This seems like a pretty big problem if the only thing we can do with BoxError is to Display or Debug it. Fortunately, there is a mechanism to get at the underlying error type. At runtime the Rust language will do its best to keep track of what type of error is in the BoxError. We can write code that, at runtime, attempts to cast the BoxError down to its underlying error type. This technique is called downcasting and the Error trait provides several functions for doing it. This example demonstrates idiomatic use of the downcast_ref() function:

fn main() -> Result<(), BoxError> {
	match count(std::path::Path::new("numbers.txt")) {
		Ok(sum) => {
			println!("The sum is: {}", sum);
			Ok(())
		}
		Err(e) => {
			if let Some(e) = e.downcast_ref::<std::io::Error>() {
				eprintln!("An I/O error of kind {:?} occured.", e.kind());
			}
			else if let Some(e) = e.downcast_ref::<std::num::ParseIntError>() {
				eprintln!("There was an error parsing an integer: {}", e);
			}
			else {
				eprintln!("An unknown error occured: {}", &e);
				eprintln!("Details: {:?}", &e);
			}
			Err(e)
		}
	}
}
The code e.downcast_ref::<std::io::Error>() returns an Option<&std::io::Error> that is Some(&std::io::Error) if, at runtime, the error e is an I/O error, or None otherwise. (By the way, the ::<>() syntax is affectionately known in Rust as the turbofish.) Within the if let Some(e) block we can treat e as reference to whatever concrete error type we attempted to downcast it to. In the final else statement we handle the case where we failed to downcast e to anything and use the generic Display and Debug traits implemented for all Error traits to print out as much information as we can.

In summary, we can match on a Result to explicitly handle errors. We can attempt to recover from the error or re-propagate it up the call stack. Each type of error implements a different set of methods for getting more specific information about that error type. When dealing with a function that could return more than one type of error at runtime through BoxError we can downcast to the underlying error types we would like to explicitly handle using, for example, downcast_ref(). We can also eprintln!("{}") any BoxError without downcasting by using the Display and Debug traits implemented for all Error trait objects. This combination of methodologies allows us to ergonomically dispatch and handle dynamically-typed errors at runtime in a way that is analagous to C++'s exceptions.

Custom Errors

Let's take the simple hello world example from the the first section and up the stakes a little bit:

use std::io::Write; // needed for write!()

fn launch(silo: u32) -> Result<(), std::io::Error> {
	let path = format!("/dev/ignition{}", silo);
	let file = std::fs::File::create(std::path::Path::new(&path))?;
	writeln!(&file, "1")
}

fn main() -> Result<(), std::io::Error> {
	launch(1) // launch missle from silo 1
}
$ cargo run
Error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
It looks like we now have a program that launches a missle. (In this early version the targeting and guidance system hasn't been implemented yet.) The user calls a function launch() that takes a silo number. The function writes 1 to /dev/ignitionN where N corresponds to the silo number. When we attempt to run the program we get an error because we don't have permission to write to /dev. (Warning: don't attempt to run this code if your computer is actually connected to a missle launcher.) The error is propagated to the operating system from main() and prints out an error message to the standard error terminal stderr.

The problem here is that the error "PermissionDenied" isn't very descriptive. Although technically our program did encounter a std::io::Error, receiving this error type from a function that launches missles doesn't make much sense to the end user. The officer who commanded the missle launch might not understand what happened. Did the missle get launched at the enemy or not? We can make our program easier to debug by creating a custom error type for missle launch failures. This is a common practice when writing Rust libraries: hide implementation details by abstracting their error types with a custom error type.

Fortunately creating a custom error type is easy. We just have to create a type that implements the Error trait. Looking at the documentation of the Error trait there are quite a few traits and methods there! However, to make a fully-functioning custom error type, we only have to implement the Display and Debug traits that control how our error is displayed with eprintln!("{}") and eprintln!("{:?}"), respectively.

mod missle {
	// needed for write!()
	use std::io::Write;

	// custom error type
	pub struct LaunchError;

	// implement Display trait
	impl std::fmt::Display for LaunchError {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			write!(f, "An error occurred while launching the missle.")
		}
	}

	// implement Debug trait
	// in this example just do the same thing as Display
	impl std::fmt::Debug for LaunchError {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			<LaunchError as std::fmt::Display>::fmt(self, f)
		}
	}

	// implement the Error trait (nothing else to do here)
	impl std::error::Error for LaunchError { }

	// private function to do the actual missle launching
	fn launch_missle_(silo: u32) -> Result<(), std::io::Error> {
		let path = format!("/dev/ignition{}", silo);
		let file = std::fs::File::create(std::path::Path::new(&path))?;
		writeln!(&file, "1")
	}

	// public function for missle launching
	// converts underlying error type to a LaunchError
	pub fn launch(silo: u32) -> Result<(), LaunchError> {
		match launch_missle_(silo) {
			Ok(ok) => Ok(ok),
			Err(_) => Err(LaunchError{})
		}
	}
}

use missle::*;

fn main() -> Result<(), LaunchError> {
	launch(1) // launch missle from silo 1
}
$ cargo run
Error: An error occurred while launching the missle.
Much better, now it's clear from the error message that the missle failed to launch! Let's step through what happened here. We create a structure LaunchError that will serve as our custom error type. We implement the Display trait for LaunchError by defining a fmt method that displays the error, in this case by printing out a simple error message. We must also implement the Debug trait, which normally would print out more detailed information; in this example we just delegate to the Display trait and print out the same message. Once the Display and Debug traits required by the Error trait are implemented we can finally implement the Error trait without actually having to do anything else inside its implementation block.

Next let's see how we use our LaunchError. We create a module mod missle for all our missle launching code and use its public members later on with use missle::*. Encapsulation through modules is a good programming practice because it saves us from confusing missle::launch() with boat::launch() or cafeteria::lunch(). In this case it also allows us to declare private functions to properly encapsulate our implmenetation details. The original launch() function is renamed launch_(). The absence of a pub keyword before the function name makes it private to the module. The trailing underscore is used to signify private implementation details by convention, but it does not have any special meaning to the compiler. We then create a public wrapper pub fn launch() that calles launch_() internally. If launch_() is successful then launch() passes through the Ok(()). If an Error occurs the resultant std::io::Error object is dropped and launch() returns a new LaunchError object instead.

Just as std::io::Error has kind() and raw_os_error() methods, so can we further customize our LaunchError. Let's add a method silo() that reports which silo was involved in the failed launch:

mod missle {
	// ...
	
	// custom error type
	pub struct LaunchError {
		silo: u32 // remember silo in which error occurred
	}
	
	// public method silo() to return the stored silo
	impl LaunchError {
		#[allow(unused)]
		pub fn silo(&self) -> u32 {
			self.silo
		}
	}
	
	// implement Display trait
	impl std::fmt::Display for LaunchError {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			write!(f, "An error occurred while launching the missle in silo {}", self.silo)
		}
	}
	
	// ...
	
	// public function for missle launching
	// converts underlying error type to a LaunchError
	pub fn launch(silo: u32) -> Result<(), LaunchError> {
		match launch_missle_(silo) {
			Ok(ok) => Ok(ok),
			Err(_) => Err(LaunchError{silo: silo})
		}
	}
}

use missle::*;

fn main() -> Result<(), LaunchError> {
	if let Err(e) = launch(1) { // launch missle from silo 1
		eprintln!("Uh-oh, there was a problem in silo {}", e.silo());
		return Err(e)
	}
	Ok(())
}
$ cargo run
Uh-oh, there was a problem in silo 1
Error: An error occurred while launching the missle in silo 1
Rather than simply being an empty structure, we declare LaunchError to have a private member variable silo. We declare a public pub fn silo() method that returns the value of silo. We also improve the Display trait message to include the silo number. Finally, when creating a new LaunchError object to return from launch(), we store the appropriate silo number in it.

In summary, creating custom error types in Rust is quite easy. We just have to implement the Error trait. The minimum amount of work to do this is to implement the Display and Debug traits to control how our error message gets rendered. (And if we're feeling really lazy we can just make Debug do the same thing as Display). It is also easy to add additional member variables and methods to our custom error type. Creating such custom error types is a good programming practice to encapsulate the errors our library might encounter in its implementation details into more descriptive and useful error messages.

Chaining

Let's continue our example of a missle launch system from the previous section. After many millions of dollars of research and development our missle launch function now includes a targeting and guidance system (which, being proprietary, are redacted from the code below). Of course, with so many more subroutines that can fail, the return type of launch_() had to be changed to a BoxError (see the section on dyn Error). We've sold hundreds of missle launchers to the military and now take support calls from a tropical island. One day we get a support call from a general: "The missle failed to launch." Of course, we expected this might happen eventually — that's why we wrote our own custom LaunchError type in the first place! We ask the general what error message he got when he tried to launch the missle. "It just said 'An error occured while launching the missle in silo 1.'" Well that doesn't give us much more information. Can't the general ship the defective launcher back to us for inspection? "Afraid not. When the missle didn't launch it got destroyed by enemy bombs."

This brings up an interesting problem. We want to encapsulate errors from our implementation details inside more decriptive errors that add context. A LaunchError is more descriptive than an std::io::Error. However, in situations like this, it would also be helpful to know the source of the LaunchError for debugging purposes. Fortunately, Rust allows us to chain errors together in this way using Error::source().

pub type BoxError = std::boxed::Box<dyn
	std::error::Error   // must implement Error to satisfy ?
	+ std::marker::Send // needed for threads
	+ std::marker::Sync // needed for threads
>;

mod missle {
	// needed for write!()
	use std::io::Write;
	
	// use BoxError inside this module
	use super::BoxError;
	
	// custom error type
	pub struct LaunchError {
		silo: u32, // remember silo in which error occurred
		source: Option<BoxError> // error (if any) that caused this error
	}
	
	// public method silo() to return the stored silo
	impl LaunchError {
		#[allow(unused)]
		pub fn silo(&self) -> u32 {
			self.silo
		}
	}
	
	// implement Display trait
	impl std::fmt::Display for LaunchError {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			write!(f, "An error occurred while launching the missle in silo {}", self.silo)?;
			if let Some(error) = &self.source {
				write!(f, "\nCaused by: {}", error)?;
			}
			Ok(())
		}
	}
	
	// implement Debug trait
	// in this example just do the same thing as Display
	impl std::fmt::Debug for LaunchError {
		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
			<LaunchError as std::fmt::Display>::fmt(self, f)
		}
	}
	
	// implement the Error trait
	impl std::error::Error for LaunchError {
		fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
			match &self.source {
				Some(error) => Some(error.as_ref()),
				None => None
			}
		}
	}
	
	// private function to do the actual missle launching
	fn launch_missle_(silo: u32) -> Result<(), BoxError> {
		let path = format!("/dev/ignition{}", silo);
		let file = std::fs::File::create(std::path::Path::new(&path))?;
		writeln!(&file, "1")?;
		// ...do some other error-prone things required to launch the missle...
		Ok(())
	}
	
	// public function for missle launching
	// converts underlying error type to a LaunchError
	pub fn launch(silo: u32) -> Result<(), LaunchError> {
		match launch_missle_(silo) {
			Ok(ok) => Ok(ok),
			Err(e) => Err(LaunchError{silo: silo, source: Some(e)})
		}
	}
}

use missle::*;

fn main() -> Result<(), LaunchError> {
	launch(1)
}
$cargo run
Error: An error occurred while launching the missle in silo 1
Caused by: Permission denied (os error 13)
This is much better! We can see that the missle failed to launch and that this was caused by a file permission error. We had to modify our LaunchError type to have an additional member, source (it could have been named anything), which is an Option<BoxError> we use to store the source of the error (or None if there is no source). Within our implementation block for the Error trait we define the method source() to return the source error. Although omitted from the example, we could have called this function from main() to programatically inspect the source of the error. The finishing touch (which is optional) is to modify our Display implementation to print out the source of the error if there is one.

By storing errors within errors and implementing Error::source() we can chain errors together ad infinitum, effectively creating a backtrace. You could imagine output like:

$ cargo run
Error: An error occurred while launching the missle in silo 1
Caused by: Guidance system error
Caused by: Invalid target coordinates (123, 45b)
Caused by: Error parsing string into an integer: 45b
Caused by: invalid digit found in string

In summary, you can chain errors togther by storing the source of the error inside your custom error object and implementing Error::source(). You can chain multiple errors together to create a kind of backtrace. By chaining errors togther you can add context to each error as it is propagated up the call stack, but without losing the detail of the original error message. This is good programming practice and should be employed whenever possible!

enum Error

If you've read this tutorial straight through then by this point you have become quite expert at handling errors in Rust and can safely skip this section. Up until now we have handled situations where a function could return more than one error type using dyn Error trait objects through the BoxError type (refer back to the section on dyn Error). This strategy has two theoretical disadvantages:

  1. Creating the BoxError involves extra memory allocation on the heap.
  2. The caller cannot discern from our function definition what types of error it might return because a BoxError can be any object that implements the Error trait.
The very curious reader may be wondering if we couldn't get around these issues using an enum. You can, and if we adapt the previous counting example from the dyn Error section it looks something like this:
use std::io::BufRead; // needed for lines()

// declare enum to store all possible error types from count()
enum SomeError {
	IOError(std::io::Error),
	NumParseIntError(std::num::ParseIntError)
}

// implement From trait to convert from each error type into the enum
impl From<std::io::Error> for SomeError {
	fn from(error: std::io::Error) -> Self {
		SomeError::IOError(error)
	}
}
impl From<std::num::ParseIntError> for SomeError {
	fn from(error: std::num::ParseIntError) -> Self {
		SomeError::NumParseIntError(error)
	}
}

// implement the Error trait on the enum itself
impl std::fmt::Display for SomeError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			SomeError::IOError(e) => write!(f, "{}", e),
			SomeError::NumParseIntError(e) => write!(f, "{}", e) 
		}
	}
}
impl std::fmt::Debug for SomeError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		<SomeError as std::fmt::Display>::fmt(self, f)
	}
}
impl std::error::Error for SomeError {
	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
		match &self {
			SomeError::IOError(e) => e.source(),
			SomeError::NumParseIntError(e) => e.source()
		}
	}
}

// can fail with a std::io:Error or a std::num::ParseIntError
// instead of returning a BoxError (i.e. dyn Error) return the SomeError enum
fn count(filename: &std::path::Path) -> Result<i32, SomeError> {
	let file = std::fs::File::open(filename)?;
	let file = std::io::BufReader::new(file);
	let mut sum: i32 = 0;
	for line in file.lines() {
		sum += line?.parse::<i32>()?;
	}
	Ok(sum)
}

fn main() -> Result<(), SomeError> {
	match count(std::path::Path::new("numbers.txt")) {
		Ok(sum) => {
			println!("The sum is: {}", sum);
			Ok(())
		}
		Err(e) => {
			// instead of downcasting just match on the type of the enum
			match &e {
				SomeError::IOError(e) =>
					eprintln!("An I/O error of kind {:?} occured.", e.kind()),
				
				SomeError::NumParseIntError(e) =>
					eprintln!("There was an error parsing an integer: {}", e)
			}
			Err(e)
		}
	}
}
To make it work we created a enum SomeError that can be either one of the two underlying error types returned by count(). To make it play nicely with the question mark ? operator we implement the Error trait on SomeError so that it too can be treated as an error. We also have to implement the From trait to automatically convert from the underlying error type into the enumeration.

Although this technically works, there are several reasons that this strategy isn't idiomatic and why BoxError is generally preferred. Many of these issues could be overcome by anonymous sum types, but there are no plans to implement this feature in rust anytime soon (if ever).

  • There's an awful lot of boilerplate code for an enumeration that handles just two errors. If you were going to use this often you would probably want a macro to automatically generate these enum types for you.
  • If you are writing a library with more than one function, which possible errors should you include to include the enumeration? Should you write a separate enumeration for each function? One enumeration to cover all the possible error types returned by your library? In the former case, what are you going to name all the different enum types?
  • If you are using error chaining, then calling source() will probably require a heap allocation and can return any kind of error anyway.
  • In terms of performance you optimistically save a heap allocation vs putting an error trait object in a box, but pessimistically you could actually take a big performance hit. A Rust enum is a lot like a C union. It's size will be that of the largest type it could possibly contain. If even one of the error types you could return is large then you might end up passing a very large Result sized to contain the worst-case-scenario error up the stack every time your function is called, whereas you might have gotten away with just a few extra bytes in the Result for storing a box.

In summary, enum Error is an alternative to dyn Error for returning more than one possible error type from a function. In theory it allows you to explicitly delineate which error types can be returned, and it avoids the heap allocation of a BoxError. Practically it is difficult to implement and actually might come with performance penalties in certain situations. Error enumerations might have niche applications in embedded code, but most of the time you should just use BoxError.

Conclusion

Rust has a robust, flexible, and ergonomic framework for handling errors. All programs can (and will) encounter errors, therefore error handling is ubiquitous. You should know how to write code that models best practices for error handling.

  • Use the question mark ? operator to propagate errors up the call stack.
  • Don't panic with unwrap() or expect() except in rare cases when the error could only have arisen through a programming mistake and trying to recover from it risks data corruption. When you panic!() your program's stack is not unwound, objects' Drop traits are not invoked, and consequently resources are not cleaned up.
  • When writing a function that can return more than one type of error, use BoxError, which is a boxed dyn Error trait object with Send and Sync. Although Send and Sync are not strictly required by the Error trait, they are important to include for interoperability with multithreaded code.
  • When handling a BoxError you can always use its Display (or Debug) traits to eprintln!("{}") an error message. For more sophisticated error handling use Error::downcast_ref() to downcast to the underlying error type.
  • Writing custom error types is as easy as implementing the Display and Debug traits. You can augment your custom error type with additional members and methods if you want to. You can and should write custom error types to encapsulate implementation details of your library.
  • Chaining errors is as easy as implementing the Error::source() method on your custom error type. Chaining errors is a good way to wrap an underlying error with additional context before propagating it up the call stack.
  • Most of the time you should just use BoxError to return more than one type of error from a function. However, there are niche applications where you might want to define a custom error type that is an enum capable of containing multiple underlying error types.

Comments

I wish I had read this six months ago, before I learned all this stuff myself the hard way! It's both concise and comprehensive!
One nit: in your downcasting example, you take it as a reference to whatever conrete error type the downcast was attempted to.
jhwgh1968
Just finished reading through, I found that really helpful thank you.
Will definitely start using BoxErrorand glad to see the error handling isn't as complicated as the multitude of error crates might suggest.
drmason13
Thanks for writing this. I did see one thing worth clarifying, namely:
When you panic!() your program's stack is not unwound, objects' Drop traits are not invoked, and consequently resources are not cleaned up.
I’m pretty sure this is true in panic=abort mode, but false by default, so these “not”s should be “may not be”s (among other tweaks to the other panic-related sentences). And the section of the post about panicking should probably say explicitly that it’s a “may” because of these panic modes.
lxrec