Error handling is a crucial part of any programming language, and Rust provides a unique and powerful approach to error handling. Unlike many other programming languages, Rust does not offer exceptions but provides the Result enum, which forces developers to handle all errors consistently and predictably, making it easier to identify and diagnose errors.

Because Rust does not have exceptions, every function must either return a value or “panic”. When a function panics, the process exits immediately and provides some feedback to the caller. It is technically possible to catch panics in Rust using catch_unwind, but this is not recommended for general use. Instead, Rust offers the Result enum, which forces all errors to be handled by the developer

This blog post will look at idiomatic error-handling patterns in Rust and help you understand the basics. We’ll go over the Result enum, how it’s used to handle errors in Rust programs, and some common crates that make this process easier.

The Result Type

If a function is fallible—that is, it can fail somehow—then it usually returns Rust’s Result type. When returning Result from a function, the developer must return one of its two variants which are Result::Ok and Result::Err. Because these variants are so common, they are made available in the prelude, so you can simply write Ok or Err.

Since Result is an enum, it’s quite straightforward to match on the error and handle either case:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let result = fallible(false);
    let value = match result {
        Ok(value) => value,
        Err(err) => {
            return Err(err);
        }
    };

    println!("got a value: {value}");
    Ok(())
}

As you can see, we can use Rust’s standard match operator to branch on either enum variant. In Rust, instead of throwing an exception, a function can either panic (which should ideally never happen) or return the Result type. This contrived example always returns an error, but you can imagine a more complicated function that could fail in some unexpected way.

Rust even allows the main function to return Result. If the returned value from main is an error, Rust will print the error using the Debug representation of the error and exit the process with an error code.

The Question Mark Operator

When a codebase uses Result heavily, handling every error case can be burdensome. To overcome this burden, Rust offers the question mark operator, which is a shorthand for unwrapping a successful outcome or returning the error to the caller. In essence, this question mark operator propagates—or “bubbles up”—the error to the caller.

For example, we can simplify the previous example significantly:

fn fallible(succeed: bool) -> Result<&'static str, &'static str> {
    if succeed {
        return Ok("success!");
    }
    Err("this is an error message")
}

fn main() -> Result<(), &'static str> {
    let value = fallible(false)?;
    println!("got a value: {value}");
    Ok(())
}

This example is equivalent to the first, but we use the ? operator to easily extract the intended value from Result::Ok if it exists; otherwise return the Result::Error variant to the caller.

Boxing Errors

In the examples so far, we’ve been using simple strings for the returned error. In a more complicated scenario, it’s more likely that you’ll encounter an error type that represents the different types of errors that can be returned.

Consider the following example:

use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let website_text = get("https://www.rust-lang.org")?.text()?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

This example downloads the content from https://www.rust-lang.org and we are using the ? operator here to simplify our error handling. Notice how the second type passed to each Result is now reqwest::Error. This type is suitable because both get() and text() return a Result with an error type of reqwest::Error.

Now, say you want to also store the downloaded website text in a file for some reason. We can easily update our code to do that too:

use tempfile::tempfile;
use std::io::copy;
use reqwest::blocking::get;

fn download() -> Result<String, reqwest::Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), reqwest::Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

But this doesn’t compile! Compiling the code above produces the following error:

 |
5 | fn download() -> Result<String, reqwest::Error> {
  |                  ------------------------------ expected `reqwest::Error` because of this
6 |     let mut file = tempfile()?;
  |                              ^ the trait `From<std::io::Error>` is not implemented for `reqwest::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `FromResidual<R>`:
            <Result<T, F> as FromResidual<Result<Infallible, E>>>
            <Result<T, F> as FromResidual<Yeet<E>>>
  = note: required for `Result<String, reqwest::Error>` to implement `FromResidual<Result<Infallible, std::io::Error>>`

This error occurs because tempfile() returns an error type that doesn’t match our Result<String, reqwest::Error> function signature. The easiest way to solve this problem is to “box” the error and return Result<String, Box<dyn std::error::Error>> instead.

The standard library provides this std::error::Error trait to handle cases like this where we need to return multiple error types. Error types exposed by libraries should implement Error so we can easily convert several different error types to the generic trait object. Again, the question mark operator facilitates this so that it just works.

use reqwest::blocking::get;
use std::error::Error;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Box<dyn Error>> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Box<dyn Error>> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

It’s also worth mentioning anyhow, which is another popular crate that offers a similar experience to using the Box<dyn Error>> trait object approach with some added features for developer ergonomics.

Using thiserror

The downside to using anyhow or Box<dyn Error>> is that we lose the types of errors that are returned by functions and these are often helpful for writing branching logic based on what kind of error occurred.

The common convention for maintaining error context is to construct a custom error enum describing the possible errors that occur in the crate or module.

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;

fn download() -> Result<String, Error> {
    let mut file = tempfile().map_err(|_| Error::File)?;
    let website_text = get("https://www.rust-lang.org")
        .map_err(|_| Error::Download)?
        .text()
        .map_err(|_| Error::Download)?;
    copy(&mut website_text.as_bytes(), &mut file).map_err(|_| Error::File)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug)]
enum Error {
    File,
    Download,
}

Now, the caller has the context of the type of error produced in the download function. However, we need to manually convert the errors to our custom Error enum by using map_err, which is once again burdensome and verbose.

Thiserror makes it easier to map error types onto your custom error types:

use reqwest::blocking::get;
use std::io::copy;
use tempfile::tempfile;
use thiserror::Error;

fn download() -> Result<String, Error> {
    let mut file = tempfile()?;
    let website_text = get("https://www.rust-lang.org")?.text()?;
    copy(&mut website_text.as_bytes(), &mut file)?;
    Ok(website_text)
}

fn main() -> Result<(), Error> {
    let value = download()?;
    println!("got a value: {value}");
    Ok(())
}

#[derive(Debug, Error)]
enum Error {
    #[error("file error: {0}")]
    File(#[from] std::io::Error),
    #[error("download error: {0}")]
    Download(#[from] reqwest::Error),
}

Notice how we’re able to keep the mapping of errors onto our custom enum declarative using the macros provided by thiserror, and our code remains clean with the ability to propagate errors using the question mark operator simply.

Conclusion

Error handling is essential in any programming language, and Rust error handling provides a powerful approach. Unlike many other programming languages, Rust does not use exceptions at all, instead relying on the Result enum to force developers to handle all errors. This approach to Rust error handling is not only more dependable and effective, but it also makes development more pleasant. The Result enum makes it simple to comprehend and handle error conditions in code, which is a key strength of Rust's error handling system.

I hope this post is helpful. If you have any questions or feedback, please leave a comment or find me on Twitter @nbrempel. If you found this post valuable, please consider sharing it!