Custom errors in Rust

Laurent Boumendil
6 min readMay 11, 2024

One of the best features of Rust is the way it handles Results and Options. Indeed, unlike other programming languages, a variable in Rust cannot be None or nil. This is in contrast with python Go, JavaScript and so on... But what does that mean?

To put it simply, let’s focus on Result first. When a function can return either a successful value or raise an error, one can have the function return a Result<Value, Error>. This is actually a struct with a few built-in methods to work with. This forces the program to handle the error. The easiest way, but should only be used for debugging, is to call the method .unwrap() which will return the Value. However, if the returned object is actually an error, the program will panic and exit.

The code below is a simple example, where we try to parse an enum. When doing so, the string might be wrong and thus the parsing function must return the error.

#[derive(Debug)]
enum Selection {
A,
B
}

fn parse_string(value: &str) -> Result<Selection, std::fmt::Error> {
match value {
"a" => Ok(Selection::A),
"b" => Ok(Selection::B),
_ => Err(std::fmt::Error)
}
}

fn main() {
let m = parse_string("a").unwrap(); // This will return the result and unwrap to get the value
println!("{m:?}");

let n = parse_string("c").unwrap(); // This will return an error (division by 0), and the unwrap will crash the program
println!("{n:?}");
}

Errors

Exited with status 101

Standard Error

Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
Running `target/debug/playground`
thread 'main' panicked at src/main.rs:19:31:
called `Result::unwrap()` on an `Err` value: Error
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Standard Output

A

But of course, using the std::fmt::Error is not the useful way of doing it for multiple reasons.

  • It is quite generic and thus, when many error types are implemented within the program, it will be difficult to use them for our benefit
  • There is no information regarding the error, as the std-err shows, it only states that there was an error.

Overall, there must be a better solution, and there is.

Lib vs Bin

Before implementing a custom error module, I want to show what I personally do. My rule of thumb is that for libs, I make a custom module, for bins, I use anyhow.

anyhow is a crate that enables simple generic errors that basically wrap any error using a macro. This is very useful when writing a bin program. Indeed, if the function you write is not available as a public API, it is less important that the error is specific to your program. It's actually similar to the code I showed above, though in a cleaner way. For your libs, especially for the public API, a custom well-structured error is best so your users (even yourself) will better understand the errors.

anyhow: a generic crate

Let’s first look at anyhow, with the example above.

use anyhow::{anyhow, Result};

#[derive(Debug)]
enum Selection {
A,
B
}

fn parse_string(value: &str) -> Result<Selection> {
match value {
"a" => Ok(Selection::A),
"b" => Ok(Selection::B),
_ => Err(anyhow!("invalide value '{value}'"))
}
}

fn main() {
let m = parse_string("a").unwrap(); // This will return the result and unwrap to get the value
println!("{m:?}");

let n = parse_string("c").unwrap(); // This will return an error (division by 0), and the unwrap will crash the program
println!("{n:?}");
}

Errors

Exited with status 101

Standard Error

Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/playground`
thread 'main' panicked at src/main.rs:21:31:
called `Result::unwrap()` on an `Err` value: invalide value 'c'
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Standard Output

A

Now you can see a similar output, though now the error message using the anyhow! macro allows use to write a better description even integrated the parameter value which is shown in the std err. You can also see that the return type of the function is now only Result<T>, instead of Result<T, E>. Indeed, we have imported a new Result struct from anyhow that overwrites the std one.

What’s cool also is that you can have the main() function return a anyhow result, and thus use the ? symbol instead of the unwrap to handle the error.

use anyhow::{anyhow, Result};

#[derive(Debug)]
enum Selection {
A,
B
}

fn parse_string(value: &str) -> Result<Selection> {
match value {
"a" => Ok(Selection::A),
"b" => Ok(Selection::B),
_ => Err(anyhow!("invalide value '{value}'"))
}
}

fn main() -> Result<()> {
let m = parse_string("a")?; // This will return the result and unwrap to get the value
println!("{m:?}");

let n = parse_string("c")?; // This will return an error (division by 0), and the unwrap will crash the program
println!("{n:?}");

Ok(())
}

You do have to finish the main() function with Ok(()) so as to return an empty valid result.

Errors

Exited with status 1

Standard Error

Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/playground`
Error: invalide value 'c'

Standard Output

A

The only issue that I have not yet solved is the fact that the terminal does not show any information about where the error originated. So this must be handled with logs, unless there is a method I have missed.

Custom error struct

In order to make a custom error struct, you can look at the code below. We use a enum to list the types of error we have. This can be custom errors, but also errors from other libs (e.g. IoError). We also give the String type to some of the errors so we can parse the messages to the error.

We then implement the fmt::Display trait so as to be able to print the error to stderr. To do this we use a match statement that deals with each error. The same method could be used to see errors to databases, etc... Most importantly, we give the error::Error trait to our struct so as to have the compiler understand it is an error type.

Finally, we can implement errors from other libraries to enable the use of ? when we need to return a MyCustomError error.

use std::fmt;
use std::error;

#[derive(Debug)]
pub enum MyCustomError {
NoAttributes,
InvalidAttributes(String),
IoError(String),
}



impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyCustomError::NoAttributes => write!(f, "No attributes found"),
MyCustomError::InvalidAttributes(ref msg) => write!(f, "Invalid attributes: {}", msg),
MyCustomError::IoError(ref msg) => write!(f, "IoError: {}", msg),
}
}
}

impl error::Error for MyCustomError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}

impl From<std::io::Error> for MyCustomError {
fn from(error: std::io::Error) -> Self {
MyCustomError::IoError(error.to_string())
}
}

#[derive(Debug)]
enum Selection {
A,
B
}

fn parse_string(value: &str) -> Result<Selection, MyCustomError> {
match value {
"a" => Ok(Selection::A),
"b" => Ok(Selection::B),
=> Err(MyCustomError::InvalidAttributes(format!("invalide value '{value}'").tostring()))
}
}

fn main() {
let m = parse_string("a").unwrap(); // This will return the result and unwrap to get the value
println!("{m:?}");

let n = parse_string("c").unwrap(); // This will return an error (division by 0), and the unwrap will crash the program
println!("{n:?}");
}

Errors

Exited with status 101

Standard Error

Compiling playground v0.0.1 (/playground)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/playground`
thread 'main' panicked at src/main.rs:62:31:
called `Result::unwrap()` on an `Err` value: InvalidAttributes("invalide value 'c'")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Standard Output

A

Now is we look at the output, we see our error type is printed to the stderr thanks to the fmt::Display trait. I recommend putting the error code in a dedicated module, so it can easily grow as it needs. Furthermore, certain lib will through an error from the compiler and actually require the use of a dynamic box, such as the hdf5 lib.

impl From<Box<dyn std::error::Error>> for MyCustomError {
fn from(error: Box<dyn std::error::Error>) -> Self {
MyCustomError::Hdf5Error(error.to_string())
}
}

As final words, use chatgpt to help you build on this basic code. If you give this as a starting point, it will help you grow your struct to cover your needs. This is also true if you want to play with macros in order to make some errors shorter, for instance.

--

--

Laurent Boumendil

Physicist specialised in nanotechnologies — python passionate