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),
}