The simplest guide to error handling in Rust

Rust is loved for its reliability, and a good chunk of its reliability comes from its error handling ergonomics.

I know that there already are a few guides about error handling in Rust, but I found these guides to be too long and not straight to the point.

So here is the simplest and most straightforward guide to learn how to handle errors in Rust. The guide I would have loved to have if I started Rust today.

Overview

There are 2 types of errors in Rust:

  • Non-recoverable errors (e.g., non-checked out of bounds array access)
  • Recoverable errors (e.g., function failed)

Non-recoverable errors

For errors that can't be handled and would bring your program into an unrecoverable state, we use the panic! macro.

fn encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
  if key.len() != 32 {
    panic!("encrypt: key length is invalid");
  }
  // ...
}

An alternative way to trigger a panic is to use the assert! macro.

fn encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
  assert!(key.len() == 32, "encrypt: key length is invalid");
  // ...
}

That being said, handling errors in Rust is very ergonomic, so I see no good reason to ever intentionally panic.

Recoverable errors

Errors that are meant to be handled are returned with the Result enum.

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

For example:

// Here, our error type is `String`
fn ultimate_answer(guess: i64) -> Result<(), String> {
  if guess == 42 {
    return Ok(());
  }
  return Err("Wrong answer".to_string());
}

Now, returning a String as an error is not really useful. Indeed, the same function may return many different errors, so it becomes harder and harder to handle them with precision:

fn ultimate_answer(guess: i64) -> Result<(), String> {
  if guess == 42 {
    return Ok(());
  } else if guess > 39 && guess <= 41 {
      return Err("A little bit more".to_string());
  } else if guess <= 45 && guess > 42 {
    return Err("A little bit less".to_string());
  }
  return Err("Wrong answer".to_string());
}

Or, the same error can be returned by many different functions:

fn do_something() -> Result<(), String> {
  // ...
  return Err("Something went wrong".to_string());
}

fn do_something_else() -> Result<(), String> {
  // ...
  return Err("Something went wrong".to_string());
}

fn do_another_thing() -> Result<(), String> {
  // ...
  return Err("Something went wrong".to_string());
}

This is where we need to define our own Error enum. Usually, we define 1 Error enum by crate.

pub enum Error {
  WrongAnswer,
  More,
  Less,
}

fn ultimate_answer(guess: i64) -> Result<(), Error> {
  if guess == 42 {
    return Ok(());
  } else if guess > 39 && guess <= 41 {
      return Err(Error::More);
  } else if guess <= 45 && guess > 42 {
    return Err(Error::Less);
  }
  return Err(Error::WrongAnswer);
}

Then, we may want to standardize the error message for each error case. For this, the community has settled on the thiserror crate.

#[derive(thiserror::Error)]
pub enum Error {
  #[error("Wrong answer")]
  WrongAnswer,
  #[error("A little bit more")]
  More,
  #[error("A little bit less")]
  Less,
}

Thanks to thiserror::Error, your Error enum now implements the std::error::Error trait and thus also the Debug and Display traits.

Then we can handle a potential error with match.

fn question() -> Result<(), Error> {
  let x = // ...
  match ultimate_answer(x) {
    Ok(_) => // do something
    Err(Error::More) => // do something
    Err(Error::Less) => // do something
    Err(Error::WrongAnswer) => // do something
  }
  // ...
}

Or, the most common way to handle errors, forward them with ?.

fn question() -> Result<(), Error> {
  let x = // ...
  ultimate_answer(x)?; // if `ultimate_answer` returns an error, `question` stops here and returns the error.
  // ...
}

Which is a shortcut for:

fn question() -> Result<(), Error> {
  let x = // ...
  match ultimate_answer(x) {
    Ok(_) => {},
    Err(err) => return Err(err.into()),
  };
  // ...
}

Error conversion

Your program or library may use many dependencies, each with its own error types, but in order to be able to use ?, your Error type needs to implement the From trait for the error types of your dependencies.

#[derive(Error, Debug, Clone)]
pub enum Error {
    #[error("Internal error.")]
    Internal(String),
    #[error("Not found.")]
    NotFound,
    #[error("Permission Denied.")]
    PermissionDenied,
    #[error("Invalid argument: {0}")]
    InvalidArgument(String),
}

impl std::convert::From<std::num::ParseIntError> for Error {
    fn from(err: std::num::ParseIntError) -> Self {
        Error::InvalidArgument(err.to_string())
    }
}

impl std::convert::From<sqlx::Error> for Error {
    fn from(err: sqlx::Error) -> Self {
        match err {
            sqlx::Error::RowNotFound => Error::NotFound,
            _ => Error::Internal(err.to_string()),
        }
    }
}

// ...

Unwrap and Expect

Finally, you can panic on recoverable errors with .unwrap() and .expect()

fn do_something() -> Result<(), Error> {
  // ...
}

fn main() {
  // panic if do_something returns Err(_)
  do_something().unwrap();
}

// or

fn main() {
  // panic if do_something returns Err(_) with the message below
  do_something().expect("do_something returned an error");
}
1 email / week to learn how to (ab)use technology for fun & profit: Programming, Hacking & Entrepreneurship.
I hate spam even more than you do. I'll never share your email, and you can unsubscribe at any time.

Tags: hacking, programming, rust, tutorial

Want to learn Rust, Cryptography and Security? Get my book Black Hat Rust!