Handling Errors in Rust

Written on

This post is day 12 of me taking part in the #100DaysToOffload challenge.

Rust uses a pretty interesting way to deal with errors. Languages like Python and JavaScript allow you to throw errors completely unchecked. C does error handling through the errno which is also completely unchecked and not enforced in any way by the programming language. This means any function you call may throw an error, and other than documentation there’s nothing to let you know that it might do so.

Worse, most of these languages don’t tell you what kind of error a function might throw. That’s obvious in JavaScript, and TypeScript doesn’t help with it either (errors caught have any or unknown type). C++ and Python let you catch specific types of errors, but you have to rely on documentation to know which types those are.

try {
  // ...
} catch (err: any) {
  // Is it a file error? Did I access something undefined? Who knows.

Java, on the other hand, requires you to explicitly mark what errors a function can throw and enforces that you handle all the errors or explicitly mark that you are propagating it.

public class Main {
  static void example() throws ArithmeticException;
}

Rust is a lot closer to this as it enforces that you handle errors. The main difference is that instead of using a special syntax for error handling, it’s built into the return type of the function directly. Which is why you’ll see functions like this:

fn example() -> Result<String, io::Error>;

This sometimes makes error handling a little harder, but luckily we have many tools that can help us handle errors.

When you don’t care about the error

Sometimes you just don’t care about the error. Perhaps the error is impossible to recover from and the best you can do is print an error message and exit, or how you handle it is the same regardless of what the error is.

To just exit

Two great options in this case is die and tracing-unwrap. Both of these options allow you to unwrap a Result type, and print a message if it’s an Error and exit. die allows you to pick the error code to exit with, while tracing-unwrap uses the tracing logging framework. You can also always use the built-in unwrap or expect functions.

// die
let output = example().die_code("some error happened", 12);
// tracing-unwrap
let output = example().unwrap_or_log()

If you are writing a function that might return any type of error, then anyhow is your best option.

fn main() -> anyhow::Result<()> {
  let output = example()?;
}

When you do care about the error

If you do care about what type of error you return, then you need thiserror. thiserror allows you to write your own error types and propagate errors.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ExampleError {
    #[error("The error message for this type")]
    Simple(String),
    #[error("An error that you are propagating")]
    FileError(#[from] io::Error),
}