DEV Community 👩‍💻👨‍💻

Sylvain Kerkour
Sylvain Kerkour

Posted on • Originally published at kerkour.com

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)

Want to learn Rust, applied Cryptography and Security? Take a look at my book Black Hat Rust.
Get 42% off until Friday, November 11 with the coupon 1311B892

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");
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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");
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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),
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Then, we may want to standardize the error message for each error case. For this, the community has (currently) 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,
}
Enter fullscreen mode Exit fullscreen mode

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
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Which is a shortcut for:

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

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(thiserror::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()),
        }
    }
}

// ...
Enter fullscreen mode Exit fullscreen mode

Unwrap and Expect

You can panic on recoverable errors with .unwrap() and .expect() which may be useful when doing exploratory programming, but should be used carefully for programs intended for production. A good practice is to add a comment near each unwrap or expect to explain why it's safe to use it.

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");
}
Enter fullscreen mode Exit fullscreen mode

Returning errors from main

Finally, you may want to return errors from your main function.

There are two ways to do this:

You can use the anyhow which will allow you to return an error of any type implementing the std::error::Error trait and will display a nicely formatted error message if the program exits with an error.

#[derive(thiserror::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),
}

// ...

fn main() -> Result<(), anyhow::Error> {
    my_function()?;

    anothercrate::function_that_return_another_error_type()?;

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Or, since Rust 1.61, you can implement the std::process::Termination for your error type to exit with custom error codes.

use std::process::{ExitCode, Termination};

pub enum MyError {
    Internal,
    Other,
}

impl Termination for MyError {
    fn report(self) -> ExitCode {
        match self {
          Internal => ExitCode::from(1),
          Other => ExitCode::from(255),
        }
    }
}

// ...

fn main() -> Result<(), MyError> {
    my_function()?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Want to learn Rust, applied Cryptography and Security? Take a look at my book Black Hat Rust.
Get 42% off until Friday, November 11 with the coupon 1311B892

Top comments (0)

🤔 Did you know?

🌚 You can turn on dark mode in Settings