Rust Error Handling Cheatsheet — Result handling functions
Introduction to Rust error handling
Rust error handling is nice but obligatory. Which makes it sometimes plenty of code.
Functions return values of type Result that is «enumeration». In Rust enumeration
means complex value that has alternatives and that alternative is shown with a tag.
Result is defined as Ok or Err. The definition is generic, and both alternatives have
an attribute that can be of any type.
enum Result<T, E> { Ok(T), Err(E), }
Normal way to handle errors we use the match statement:
match function_to_do_nice_things() { Ok(t) => { println!("this went great, {}", t); }, Err(e) => "oops" }
Method chaining in error handling
If all the errors are wrapped to match statement, we get soon plenty of match
statements inside each other.
One alternative to address this is to chain methods to handle the errors.
value = function_to_do_nice_things() .and_then(other_function) .map_err(|e| module_error_from_io_error(e))?
Example calls two functions, gets error value from the first that fails, maps it from io error to
our own error and returns it from the function to the caller. If calls are ok, we unwrap the
Ok result and assign it to variable value.
It is pretty difficult to find the right error mapping funtion. There are 21 of them
and the descriptions are pretty cryptic.
Cheatsheet
This cheatsheet lists 21 error Result handling functions and tells what they do.
The division is
- Six functions that map results to results
- Two functions that map results to values (or results)
- Eight functions that can be used to extract Ok value from the Result or to get information on existence of Ok result
- Four functions that can be used to extract Err value or to get information of existence of Err result
- One special conversion function
The first column tells what is done to the result if it is Ok variant. The second column tells what is done to the Err variant.
This hopefully makes it easier to find the right function for specific purpose.
For example, if you are looking for a function that leaves Err result as is and maps the Ok result with a function, you quickly
find that map and and_then are such functions. Then you decide if you want to map simply with a function mapping the value (map)
or if you want to return a full Ok/Err result with and_then function.
In these examples
- r is the result what these functions address
- t is the Ok value inside r, Ok(t)
- e is the Err value inside r, Err(e)
- r2 is the second result given as an argument having t2 and e2
- f is a function that gets t as input and generates t’
- F is a function that gets t as input and generates new Result(t’, e’)
- g is a function that gets e as input and generates e’
Mapping result to result
Ok(t) -> ? | Err(e) -> ? | Code r: | Description |
---|---|---|---|
t -> Ok(t’) | Unchanged | r.map(|t| f(t)) |
Map ok with function, error as is, mapping can not result error |
t -> (t’, e’) | Unchanged | r.and_then(|t| F(t)) |
Calls function for Ok value and propagates errors. When you chain these like r.and_then().and_then(), it returns result of last function or the first happened error. |
Unchanged | _e -> (t’, e’) | r.or_else(|_e| F()) |
In chain r.or_else(f1).or_else(f2) calls functions until one succeeds, does not call after first success, argument must return Result type. Called function gets the error value as argument but likely do not use it. |
Unused, return arg r2 (t2, e2) | Unchanged | r.and(r2) |
In chain r.and(r2).and(r3) return last ok result or first error |
Unchanged | Unused, return arg r2 (t2, e2) instead | r.or(r2) |
In chain r.or(r2).or(r3) return value of first Ok or last error, evaluates all or values |
Unchanged | e -> Err(e’) | r.map_err(|e| g(e)) |
Map error with g(e), that return a normal type that is automatically converted to error result. Map function can not return an error. |
Mapping Result to any type
Ok -> ? | Err -> ? | Code | Description |
---|---|---|---|
t -> t’ (returned as is) | e -> e’ (returned as is) | r.map_or_else(|e| g(e), |t| f(t)) |
Map both Ok and Err with a function. Result can be of any type but it has to be same for both Ok and Error. Err mapping function is first because it is considered as a «default value if normal processing fails» like in the map_or. |
t -> t’ (returned as is) | Literal (returned as is) | r.map_or(literal, |t| f(t)) |
Map with function. If error, use literal as a default value. Mapping function can return Result but also any other type that matches literal. Note that this does NOT meant that if mapping function fails, use literal. It means that if we can not use mapping function due to error, give the literal instead. |
Extract Ok value
Ok -> ? | Err -> ? | Code | Description |
---|---|---|---|
t | stop function and return Err(e) immediately | r? |
If error, return from the function using this same result. Function result must be compatible. |
t | panic | r.unwrap() |
Panics with error, may use e as panic message. |
t | panic with message | r.expect("string") |
unwrap() with a given panic message. |
t | Literal as t’ | r.unwrap_or(literal) |
Unwrap, if error, use literal from arguments instead. |
t | e -> t’ | r.unwrap_or_else(|e| g(e)) |
Extract value or derive it from error with function |
t | Default as t’ | r.unwrap_or_default() |
Returns value or default for that type (if set) |
true | false | r.is_ok() |
True if ok |
Option::Some(t) | Option::None | r.ok() |
If Ok, return Option::Some(t), in case of error returns Option::None |
Extract error
Ok -> ? | Err -> ? | Code | Description |
---|---|---|---|
panic | e | r.unwrap_err() |
Panics, may shows value of t |
panic | e | r.expect_err("message") |
Panics if ok, with set panic message, prints value of t |
false | true | r.is_err() |
True if error |
None | Some(e) | r.err() |
Some(e) if error or None if no error |
Convert
Ok -> ? | Err -> ? | Code | Description |
---|---|---|---|
t -> Some(t) | e -> Some(e) | r.transpose() |
Take Option (especially Option::None) out from Result |
Question mark operator
To use r?, function must return compatible Result type. For testing,
the main function and tests can return Result type (Rust 2018)
Own errors
It is customary to define your own Error type for your program
pub struct MyError {};
pub type Result<T> = result::Result<T, MyError>;
impl fmt::Display for MyError {
..
}
impl fmt::Debug for MyError {
..
}
Generating results for testing etc.
let r: Result<u32, String> = Ok(233);
let s: Result<u32, String> = Err("meaningless input");
let t: Result<(), ()> = Ok(());
Error handling in Rust is very different if you’re coming from other languages. In languages like Java, JS, Python etc, you usually throw
exceptions and return
successful values. In Rust, you return something called a Result
.
The Result<T, E>
type is an enum that has two variants — Ok(T)
for successful value or Err(E)
for error value:
enum Result<T, E> {
Ok(T),
Err(E),
}
Returning errors instead of throwing them is a paradigm shift in error handling. If you’re new to Rust, there will be some friction initially as it requires you to reason about errors in a different way.
In this post, I’ll go through some common error handling patterns so you gradually become familiar with how things are done in Rust:
- Ignore the error
- Terminate the program
- Use a fallback value
- Bubble up the error
- Bubble up multiple errors
- Match boxed errors
- Libraries vs Applications
- Create custom errors
- Bubble up custom errors
- Match custom errors
Ignore the error
Let’s start with the simplest scenario where we just ignore the error. This sounds careless but has a couple of legitimate use cases:
- We’re prototyping our code and don’t want to spend time on error handling.
- We’re confident that the error won’t occur.
Let’s say that we’re reading a file which we’re pretty sure would be present:
use std::fs;
fn main() {
let content = fs::read_to_string("./Cargo.toml").unwrap();
println!("{}", content)
}
Even though we know that the file would be present, the compiler has no way of knowing that. So we use unwrap
to tell the compiler to trust us and return the value inside. If the read_to_string
function returns an Ok()
value, unwrap
will get the contents of Ok
and assign it to the content
variable. If it returns an error, it will “panic”. Panic either terminates the program or exits the current thread.
Note that unwrap
is used in quite a lot of Rust examples to skip error handling. This is mostly done for convenience and shouldn’t be used in real code as it is.
Terminate the program
Some errors cannot be handled or recovered from. In these cases, it’s better to fail fast by terminating the program.
Let’s use the same example as above — we’re reading a file which we’re sure to be present. Let’s imagine that, for this program, that file is absolutely important without which it won’t work properly. If for some reason, this file is absent, it’s better to terminate the program.
We can use unwrap
as before or use expect
— it’s same as unwrap
but lets us add extra error message.
use std::fs;
fn main() {
let content = fs::read_to_string("./Cargo.toml").expect("Can't read Cargo.toml");
println!("{}", content)
}
See also: panic!
Use a fallback value
In some cases, you can handle the error by falling back to a default value.
For example, let’s say we’re writing a server and the port it listens to can be configured using an environment variable. If the environment variable is not set, accessing that value would result in an error. But we can easily handle that by falling back to a default value.
use std::env;
fn main() {
let port = env::var("PORT").unwrap_or("3000".to_string());
println!("{}", port);
}
Here, we’ve used a variation of unwrap
called unwrap_or
which lets us supply default values.
See also: unwrap_or_else
, unwrap_or_default
Bubble up the error
When you don’t have enough context to handle the error, you can bubble up (propagate) the error to the caller function.
Here’s a contrived example which uses a webservice to get the current year:
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n {}", e),
}
}
fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let result = reqwest::blocking::get(url);
let response = match result {
Ok(res) => res,
Err(err) => return Err(err),
};
let body = response.json::<HashMap<String, i32>>();
let json = match body {
Ok(json) => json,
Err(err) => return Err(err),
};
let date = json["years"].to_string();
Ok(date)
}
There are two function calls inside the get_current_date
function (get
and json
) that return Result
values. Since get_current_date
doesn’t have context of what to do when they return errors, it uses pattern matching to propagate the errors to main
.
Using pattern matching to handle multiple or nested errors can make your code “noisy”. Instead, we can rewrite the above code using the ?
operator:
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n {}", e),
}
}
fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let date = res["years"].to_string();
Ok(date)
}
This looks much cleaner!
The ?
operator is similar to unwrap
but instead of panicking, it propagates the error to the calling function. One thing to keep in mind is that we can use the ?
operator only for functions that return a Result
or Option
type.
Bubble up multiple errors
In the previous example, the get
and json
functions return a reqwest::Error
error which we’ve propagated using the ?
operator. But what if we’ve another function call that returned a different error value?
Let’s extend the previous example by returning a formatted date instead of the year:
+ use chrono::NaiveDate;
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n {}", e),
}
}
fn get_current_date() -> Result<String, reqwest::Error> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
- let date = res["years"].to_string();
+ let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+ let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
The above code won’t compile as parse_from_str
returns a chrono::format::ParseError
error and not reqwest::Error
.
We can fix this by Box
ing the errors:
use chrono::NaiveDate;
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n {}", e),
}
}
- fn get_current_date() -> Result<String, reqwest::Error> {
+ fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
Returning a trait object Box<dyn std::error::Error>
is very convenient when we want to return multiple errors!
See also: anyhow
, eyre
Match boxed errors
So far, we’ve only printed the errors in the main
function but not handled them. If we want to handle and recover from boxed errors, we need to “downcast” them:
use chrono::NaiveDate;
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
- Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n {}", e),
+ Err(e) => {
+ eprintln!("Oh noes, we don't know which era we're in! :(");
+ if let Some(err) = e.downcast_ref::<reqwest::Error>() {
+ eprintln!("Request Error: {}", err)
+ } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
+ eprintln!("Parse Error: {}", err)
+ }
+ }
}
}
fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
Notice how we need to be aware of the implementation details (different errors inside) of get_current_date
to be able to downcast them inside main
.
See also: downcast
, downcast_mut
Applications vs Libraries
As mentioned previously, the downside to boxed errors is that if we want to handle the underlying errors, we need to be aware of the implementation details. When we return something as Box<dyn std::error::Error>
, the concrete type information is erased. To handle the different errors in different ways, we need to downcast them to concrete types and this casting can fail at runtime.
However, saying something is a “downside” is not very useful without context. A good rule of thumb is to question whether the code you’re writing is an “application” or a “library”:
Application
- The code you’re writing would be used by end users.
- Most errors generated by application code won’t be handled but instead logged or reported to the user.
- It’s okay to use boxed errors.
Library
- The code you’re writing would be consumed by other code. A “library” can be open source crates, internal libraries etc
- Errors are part of your library’s API, so your consumers know what errors to expect and recover from.
- Errors from your library are often handled by your consumers so they need to be structured and easy to perform exhaustive match on.
- If you return boxed errors, then your consumers need to be aware of the errors created by your code, your dependencies, and so on!
- Instead of boxed errors, we can return custom errors.
Create custom errors
For library code, we can convert all the errors to our own custom error and propagate them instead of boxed errors. In our example, we currently have two errors — reqwest::Error
and chrono::format::ParseError
. We can convert them to MyCustomError::HttpError
and MyCustomError::ParseError
respectively.
Let’s start by creating an enum to hold our two error variants:
// error.rs
pub enum MyCustomError {
HttpError,
ParseError,
}
The Error
trait requires us to implement the Debug
and Display
traits:
// error.rs
use std::fmt;
#[derive(Debug)]
pub enum MyCustomError {
HttpError,
ParseError,
}
impl std::error::Error for MyCustomError {}
impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyCustomError::HttpError => write!(f, "HTTP Error"),
MyCustomError::ParseError => write!(f, "Parse Error"),
}
}
}
We’ve created our own custom error!
This is obviously a simple example as the error variants don’t contain much information about the error. But this should be sufficient as a starting point for creating more complex and realistic custom errors. Here are some real life examples: ripgrep, reqwest, csv and serde_json
See also: thiserror
, snafu
Bubble up custom errors
Let’s update our code to return the custom errors we just created:
// main.rs
+ mod error;
use chrono::NaiveDate;
+ use error::MyCustomError;
use std::collections::HashMap;
fn main() {
// skipped, will get back later
}
- fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
+ fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
- let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
+ let res = reqwest::blocking::get(url)
+ .map_err(|_| MyCustomError::HttpError)?
+ .json::<HashMap<String, i32>>()
+ .map_err(|_| MyCustomError::HttpError)?;
let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
- let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
+ .map_err(|_| MyCustomError::ParseError)?;
let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
Notice how we’re using map_err
to convert the error from one type to another type.
But things got verbose as a result — our function is littered with these map_err
calls. We can implement the From
trait to automatically coerce the error types when we use the ?
operator:
// error.rs
use std::fmt;
#[derive(Debug)]
pub enum MyCustomError {
HttpError,
ParseError,
}
impl std::error::Error for MyCustomError {}
impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyCustomError::HttpError => write!(f, "HTTP Error"),
MyCustomError::ParseError => write!(f, "Parse Error"),
}
}
}
+ impl From<reqwest::Error> for MyCustomError {
+ fn from(_: reqwest::Error) -> Self {
+ MyCustomError::HttpError
+ }
+ }
+ impl From<chrono::format::ParseError> for MyCustomError {
+ fn from(_: chrono::format::ParseError) -> Self {
+ MyCustomError::ParseError
+ }
+ }
// main.rs
mod error;
use chrono::NaiveDate;
use error::MyCustomError;
use std::collections::HashMap;
fn main() {
// skipped, will get back later
}
fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
- let res = reqwest::blocking::get(url)
- .map_err(|_| MyCustomError::HttpError)?
- .json::<HashMap<String, i32>>()
- .map_err(|_| MyCustomError::HttpError)?;
+ let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
- let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
- .map_err(|_| MyCustomError::ParseError)?;
+ let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
We’ve removed map_err
and the code looks much cleaner!
However, From
trait is not magic and there are times when we need to use map_err
. In the above example, we’ve moved the type conversion from inside the get_current_date
function to the From<X> for MyCustomError
implementation. This works well when the information needed to convert from one error to MyCustomError
can be obtained from the original error object. If not, we need to use map_err
inside get_current_date
.
Match custom errors
We’ve ignored the changes in main
until now, here’s how we can handle the custom errors:
// main.rs
mod error;
use chrono::NaiveDate;
use error::MyCustomError;
use std::collections::HashMap;
fn main() {
match get_current_date() {
Ok(date) => println!("We've time travelled to {}!!", date),
Err(e) => {
eprintln!("Oh noes, we don't know which era we're in! :(");
- if let Some(err) = e.downcast_ref::<reqwest::Error>() {
- eprintln!("Request Error: {}", err)
- } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
- eprintln!("Parse Error: {}", err)
- }
+ match e {
+ MyCustomError::HttpError => eprintln!("Request Error: {}", e),
+ MyCustomError::ParseError => eprintln!("Parse Error: {}", e),
+ }
}
}
}
fn get_current_date() -> Result<String, MyCustomError> {
let url = "https://postman-echo.com/time/object";
let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
let date = parsed_date.format("%Y %B %d").to_string();
Ok(date)
}
Notice how unlike boxed errors, we can actually match on the variants inside MyCustomError
enum.
Conclusion
Thanks for reading! I hope this post was helpful in introducing the basics of error handling in Rust. I’ve added the examples to a repo in GitHub which you can use for practice. If you’ve more questions, please contact me at sheshbabu [at] gmail.com. Feel free to follow me in Twitter for more posts like this
Basic Error Handling
Error handling in Rust can be clumsy if you can’t use the question-mark operator.
To achieve happiness, we need to return a Result
which can accept any error.
All errors implement the trait std::error::Error
, and
so any error can convert into a Box<Error>
.
Say we needed to handle both i/o errors and errors from converting
strings into numbers:
# #![allow(unused_variables)] # #fn main() { // box-error.rs use std::fs::File; use std::io::prelude::*; use std::error::Error; fn run(file: &str) -> Result<i32,Box<Error>> { let mut file = File::open(file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents.trim().parse()?) } #}
So that’s two question-marks for the i/o errors (can’t open file, or can’t read as string)
and one question-mark for the conversion error. Finally, we wrap the result inOk
.
Rust can work out from the return type thatparse
should convert toi32
.It’s easy to create a shortcut for this
Result
type:# #![allow(unused_variables)] # #fn main() { type BoxResult<T> = Result<T,Box<Error>>; #}
However, our programs will have application-specific error conditions, and so
we need to create our own error type. The basic requirements
are straightforward:
- May implement
Debug
- Must implement
Display
- Must implement
Error
Otherwise, your error can do pretty much what it likes.
# #![allow(unused_variables)] # #fn main() { // error1.rs use std::error::Error; use std::fmt; #[derive(Debug)] struct MyError { details: String } impl MyError { fn new(msg: &str) -> MyError { MyError{details: msg.to_string()} } } impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f,"{}",self.details) } } impl Error for MyError { fn description(&self) -> &str { &self.details } } // a test function that returns our error result fn raises_my_error(yes: bool) -> Result<(),MyError> { if yes { Err(MyError::new("borked")) } else { Ok(()) } } #}
Typing
Result<T,MyError>
gets tedious and many Rust modules define their own
Result
— e.g.io::Result<T>
is short forResult<T,io::Error>
.In this next example we need to handle the specific error when a string can’t be parsed
as a floating-point number.Now the way that
?
works
is to look for a conversion from the error of the expression to the error that must
be returned. And this conversion is expressed by theFrom
trait.Box<Error>
works as it does because it implementsFrom
for all types implementingError
.At this point you can continue to use the convenient alias
BoxResult
and catch everything
as before; there will be a conversion from our error intoBox<Error>
.
This is a good option for smaller applications. But I want to show other errors can
be explicitly made to cooperate with our error type.
ParseFloatError
implementsError
sodescription()
is defined.# #![allow(unused_variables)] # #fn main() { use std::num::ParseFloatError; impl From<ParseFloatError> for MyError { fn from(err: ParseFloatError) -> Self { MyError::new(err.description()) } } // and test! fn parse_f64(s: &str, yes: bool) -> Result<f64,MyError> { raises_my_error(yes)?; let x: f64 = s.parse()?; Ok(x) } #}
The first
?
is fine (a type always converts to itself withFrom
) and the
second?
will convert theParseFloatError
toMyError
.And the results:
fn main() { println!(" {:?}", parse_f64("42",false)); println!(" {:?}", parse_f64("42",true)); println!(" {:?}", parse_f64("?42",false)); } // Ok(42) // Err(MyError { details: "borked" }) // Err(MyError { details: "invalid float literal" })
Not too complicated, although a little long-winded. The tedious bit is having to
writeFrom
conversions for all the other error types that need to play nice
withMyError
— or you simply lean onBox<Error>
. Newcomers get confused
by the multitude of ways to do the same thing in Rust; there is always another
way to peel the avocado (or skin the cat, if you’re feeling bloodthirsty). The price
of flexibility is having many options. Error-handling for a 200 line program can afford
to be simpler than for a large application. And if you ever want to package your precious
droppings as a Cargo crate, then error handling becomes crucial.Currently, the question-mark operator only works for
Result
, notOption
, and this is
a feature, not a limitation.Option
has aok_or_else
which converts itself into aResult
.
For example, say we had aHashMap
and must fail if a key isn’t defined:# #![allow(unused_variables)] # #fn main() { let val = map.get("my_key").ok_or_else(|| MyError::new("my_key not defined"))?; #}
Now here the error returned is completely clear! (This form uses a closure, so the error value
is only created if the lookup fails.)simple-error for Simple Errors
The simple-error crate provides you with
a basic error type based on a string, as we have defined it here, and a few convenient macros.
Like any error, it works fine withBox<Error>
:#[macro_use] extern crate simple_error; use std::error::Error; type BoxResult<T> = Result<T,Box<Error>>; fn run(s: &str) -> BoxResult<i32> { if s.len() == 0 { bail!("empty string"); } Ok(s.trim().parse()?) } fn main() { println!("{:?}", run("23")); println!("{:?}", run("2x")); println!("{:?}", run("")); } // Ok(23) // Err(ParseIntError { kind: InvalidDigit }) // Err(StringError("empty string"))
bail!(s)
expands toreturn SimpleError::new(s).into();
— return early with a conversion into
the receiving type.You need to use
BoxResult
for mixing theSimpleError
type with other errors, since
we can’t implementFrom
for it, since both the trait and the type come from other crates.error-chain for Serious Errors
For non-trivial applications have a look
at the error_chain crate.
A little macro magic can go a long way in Rust…Create a binary crate with
cargo new --bin test-error-chain
and
change to this directory. EditCargo.toml
and adderror-chain="0.8.1"
to the end.What error-chain does for you is create all the definitions we needed for manually implementing
an error type; creating a struct, and implementing the necessary traits:Display
,Debug
andError
.
It also by default implementsFrom
so strings can be converted into errors.Our first
src/main.rs
file looks like this. All the main program does is callrun
, print out any
errors, and end the program with a non-zero exit code. The macroerror_chain
generates all the
definitions needed, within anerror
module — in a larger program you would put this in its own file.
We need to bring everything inerror
back into global scope because our code will need to see
the generated traits. By default, there will be anError
struct and aResult
defined with that
error.Here we also ask for
From
to be implemented so thatstd::io::Error
will convert into
our error type usingforeign_links
:#[macro_use] extern crate error_chain; mod errors { error_chain!{ foreign_links { Io(::std::io::Error); } } } use errors::*; fn run() -> Result<()> { use std::fs::File; File::open("file")?; Ok(()) } fn main() { if let Err(e) = run() { println!("error: {}", e); std::process::exit(1); } } // error: No such file or directory (os error 2)
The ‘foreign_links’ has made our life easier, since the question mark operator now knows how to
convertstd::io::Error
into ourerror::Error
. (Under the hood, the macro is creating a
From<std::io::Error>
conversion, exactly as spelt out earlier.)All the action happens in
run
; let’s make it print out the first 10 lines of a file given as the
first program argument. There may or may not be such an argument, which isn’t necessarily an
error. Here we want to convert anOption<String>
into aResult<String>
. There are twoOption
methods for doing this conversion, and I’ve picked the simplest one. OurError
type implements
From
for&str
, so it’s straightforward to make an error with a simple text message.# #![allow(unused_variables)] # #fn main() { fn run() -> Result<()> { use std::env::args; use std::fs::File; use std::io::BufReader; use std::io::prelude::*; let file = args().skip(1).next() .ok_or(Error::from("provide a file"))?; let f = File::open(&file)?; let mut l = 0; for line in BufReader::new(f).lines() { let line = line?; println!("{}", line); l += 1; if l == 10 { break; } } Ok(()) } #}
There is (again) a useful little macro
bail!
for ‘throwing’ errors.
An alternative to theok_or
method here could be:# #![allow(unused_variables)] # #fn main() { let file = match args().skip(1).next() { Some(s) => s, None => bail!("provide a file") }; #}
Like
?
it does an early return.The returned error contains an enum
ErrorKind
, which allows us to distinguish between various
kinds of errors. There’s always a variantMsg
(when you sayError::from(str)
) and theforeign_links
has declaredIo
which wraps I/O errors:fn main() { if let Err(e) = run() { match e.kind() { &ErrorKind::Msg(ref s) => println!("msg {}",s), &ErrorKind::Io(ref s) => println!("io {}",s), } std::process::exit(1); } } // $ cargo run // msg provide a file // $ cargo run foo // io No such file or directory (os error 2)
It’s straightforward to add new kinds of errors. Add an
errors
section to theerror_chain!
macro:# #![allow(unused_variables)] # #fn main() { error_chain!{ foreign_links { Io(::std::io::Error); } errors { NoArgument(t: String) { display("no argument provided: '{}'", t) } } } #}
This defines how
Display
works for this new kind of error. And now we can handle
‘no argument’ errors more specifically, feedingErrorKind::NoArgument
aString
value:# #![allow(unused_variables)] # #fn main() { let file = args().skip(1).next() .ok_or(ErrorKind::NoArgument("filename needed".to_string()))?; #}
There’s now an extra
ErrorKind
variant that you must match:fn main() { if let Err(e) = run() { println!("error {}",e); match e.kind() { &ErrorKind::Msg(ref s) => println!("msg {}", s), &ErrorKind::Io(ref s) => println!("io {}", s), &ErrorKind::NoArgument(ref s) => println!("no argument {:?}", s), } std::process::exit(1); } } // cargo run // error no argument provided: 'filename needed' // no argument "filename needed"
Generally, it’s useful to make errors as specific as possible, particularly if this is a library
function! This match-on-kind technique is pretty much the equivalent of traditional exception handling,
where you match on exception types in acatch
orexcept
block.In summary, error-chain creates a type
Error
for you, and definesResult<T>
to bestd::result::Result<T,Error>
.
Error
contains an enumErrorKind
and by default there is one variantMsg
for errors created from
strings. You define external errors withforeign_links
which does two things. First, it creates a new
ErrorKind
variant. Second, it definesFrom
on these external errors so they can be converted to our
error. New error variants can be easily added. A lot of irritating boilerplate code is eliminated.Chaining Errors
But the really cool thing that this crate provides is error chaining.
As a library user, it’s irritating when a method simply just ‘throws’ a generic I/O error. OK, it
could not open a file, fine, but what file? Basically, what use is this information to me?
error_chain
does error chaining which helps solve this problem of over-generic errors. When we
try to open the file, we can lazily lean on the conversion toio::Error
using?
, or chain the error.# #![allow(unused_variables)] # #fn main() { // non-specific error let f = File::open(&file)?; // a specific chained error let f = File::open(&file).chain_err(|| "unable to read the damn file")?; #}
Here’s a new version of the program, with no imported ‘foreign’ errors, just the defaults:
#[macro_use] extern crate error_chain; mod errors { error_chain!{ } } use errors::*; fn run() -> Result<()> { use std::env::args; use std::fs::File; use std::io::BufReader; use std::io::prelude::*; let file = args().skip(1).next() .ok_or(Error::from("filename needed"))?; ///////// chain explicitly! /////////// let f = File::open(&file).chain_err(|| "unable to read the damn file")?; let mut l = 0; for line in BufReader::new(f).lines() { let line = line.chain_err(|| "cannot read a line")?; println!("{}", line); l += 1; if l == 10 { break; } } Ok(()) } fn main() { if let Err(e) = run() { println!("error {}", e); /////// look at the chain of errors... /////// for e in e.iter().skip(1) { println!("caused by: {}", e); } std::process::exit(1); } } // $ cargo run foo // error unable to read the damn file // caused by: No such file or directory (os error 2)
So the
chain_err
method takes the original error, and creates a new error which contains the
original error — this can be continued indefinitely. The closure is expected to return any
value which can be converted into an error.Rust macros can clearly save you a lot of typing.
error-chain
even provides a shortcut that
replaces the whole main program:# #![allow(unused_variables)] # #fn main() { quick_main!(run); #}
(
run
is where all the action takes place, anyway.)
Introduction
In this article, I will discuss error handling in Rust
🦀. I try to explain the differences between recoverable and unrecoverable errors, and how to handle them properly in your code.
At the end of this article, I will also take a quick lookinto two popular crates for error handling in Rust
🦀: anyhow
and thiserror
.
The Panic Macro and Unrecoverable Errors
A Panic
is an exception that a Rust
🦀 program can throw. It stops all execution in the current thread. Panic, will return a short description of the error and the location of the panic in the source code.
Let’s look at an example:
fn main() {
println!("Hello, world!");
panic!("oh no!");
}
This will print Hello, world!
and then panic with the message oh no!
and the location of the panic in the source code.
If your running this code in a terminal, you will see the following output:
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The message gives us also a hint on how to display a backtrace. If you run the code with the environment variable RUST_BACKTRACE=1
you will get a list of all the functions leading up to the panic.
RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: rust_error::main
at ./src/main.rs:3:5
3: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
In this case, the backtrace is not very useful, because the panic is in the main
function.
Let’s look at a different example, which is extremely contrived, but for demonstration purposes, it will do.
fn a() {
b();
}
fn b() {
c("engin");
}
fn c(name: &str) {
if name == "engin" {
panic!("Dont pass engin");
}
}
fn main() {
a();
}
We have three functions a
, b
and c
. The main function calls a
. a
calls b
and b
calls c
. c
takes a string as an argument and panics if the string is engin
.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This error is not very useful. We can see that the panic happened in c
, but we don’t know which function called c
.
If we run the code with the environment variable RUST_BACKTRACE=1
we get the following output:
RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: rust_error::c
at ./src/main.rs:11:9
3: rust_error::b
at ./src/main.rs:6:5
4: rust_error::a
at ./src/main.rs:2:5
5: rust_error::main
at ./src/main.rs:16:5
6: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
This is much better. We can see that the panic happened in c
, and we can see the call stack leading up to the panic. We see that c
was called by b
, which was called by a
, which was called by main
. So let’s change the code in b
to call c
with a different name.
fn b() {
c("dirien");
}
Now the code compiles and runs without any problems.
Recoverable Errors
A recoverable error is an error that can be handled by the code. For example, if we try to open a file that does not exist, we can handle the error and print a message to the user or create the file instead of crashing the program.
For this case, we can use the Result
type. The Result
type is an enum with two variants: Ok
and Err
. The Ok
variant indicates that the operation was successful and stores a generic value. The Err
variant indicates that the operation failed and stores an error value.
Like the Option
type, the Result
type is defined in the standard library, and we need to bring it into scope.
Let’s look at an example. We will try to open a file and read the contents of the file.
fn main() {
let f = File::open("hello.txt");
}
Here we need to check the result of the open
function. If the file is opened successfully, we can read the contents of the file. If the file is not opened successfully, we can print an error message to the user.
To check the result of the open
function, we can use the match
expression. The match
expression is similar to the if
expression, but it can handle more than two cases. We’re also shadowing the f
variable and setting it to the match
expression.
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => panic!("There was a problem opening the file: {:?}", error),
};
}
If the open
function returns Ok
, we store the file handle in the f
variable. If the open
function returns Err
, we panic and print the error message.
Let us run the code and see what happens.
cargo run
warning: `rust-error` (bin "rust-error") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/rust-error`
thread 'main' panicked at 'There was a problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:7:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
We get a panic
, but the error message is much more useful. We can see that the error is Os { code: 2, kind: NotFound, message: "No such file or directory" }
. This error makes sense because we are trying to open a file that does not exist.
Now let’s enhance the code instead of panicking, we will create the file if it does not exist. First, we will bring the ErrorKind
enum into scope.
use std::fs::File;
use std::io::ErrorKind;
...
Then we will use the match
expression to check the error kind. If the error kind is NotFound
, we will create the file. But the creation of the file can also fail, so we will use the match
expression again to check the result of the create
function. If the create
function returns Ok
, we will return the file handle. If the create
function returns Err
, we will panic.
The last part is to use other_error
to handle all other errors that are not ErrorKind::NotFound
.
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
Now when we run the code, we can see that no panic happens. And if we check the directory, we can see that the file was created.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/rust-error`
But this code is not very readable. We have a lot of match
expressions. A better way to handle this is to use closures. We will use closures to handle the Ok
and Err
variants of the Result
type.
When we attempt to open a file, we will use the unwrap_or_else
method which gives us back the file or calls the anonymous function or closure that we pass the error to. Inside the closure, we will check the error kind. If the error is NotFound
then we attempt to create the file called the unwrap_or_else
method again. This gives us back the file if the calls succeed. Note that we don’t have a semicolon at the end which means this is an expression and not a statement. In the failure case, we have another closure that will just panic.
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}
Now we going to rewrite the code again to use the unwrap
and expect
methods. The unwrap method is a shortcut method that is implemented on Result
types. If the Result
is Ok
, the unwrap
method will return the value inside the Ok
. If the Result
is Err
, the unwrap
method will call the panic!
macro for us.
fn main() {
let f = File::open("hello.txt").unwrap();
}
When we run the code, we get the same error as before.
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/rust-error`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The expect
method is similar to the unwrap
method, but we can pass a custom error message to the expect
method. This error message will be printed when the Result
is Err
.
fn main() {
let f = File::open("hello.txt").expect("OMG! I cant open the file!");
}
When we run the code, we can see our custom error message.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/rust-error`
thread 'main' panicked at 'OMG! I cant open the file!: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
How to propagate errors
In the previous section, we saw how to handle errors. But what if we want to propagate the error to the caller of our function? This gives the caller the ability to handle the error.
Let’s say we want to read the contents of a file. We will create a function that reads username
from a file. The function will return a Result
type. The Result
type will contain a String
on success and the io::Error
on error.
If the file does not exist, we will return the error. If the file exists, we will try to read the contents of the file. If this is not successful, we will return the error. If the read is successful, we will return the username.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
We can shorten the code by using the ?
operator. The ?
operator can only be used in functions that return a Result
type. The ?
operator is similar to our unwrap
and expect
methods. If the Result
is Ok
, the ?
operator will return the value inside the Ok
. If the Result
is Err
, instead of calling the panic!
macro, the ?
operator will return the error and early exit the function.
If everything is successful, the ?
operator will return safely the value inside the Ok
.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
We can shorten the code even more by chaining method calls. The ?
operator can be used with method calls that return a Result
type.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
But we can make the code even shorter by using the system module function fs::read_to_string
. The fs::read_to_string
function will open the file, create a new String
, read the contents of the file into the String
, and return it. If any of these steps fail, the fs::read_to_string
function will return the error.
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
As mentioned before, the ?
operator can only be used in functions that return a Result
type. If we want to use the ?
operator in the main
function, we have to change the return type of the main
function to Result
. The main
function can also return a Result
type.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
The main
function returns a Result
type. The Result
type contains a ()
on success and a Box<dyn Error>
on error.
Error helper crates
There are a lot of crates that can help you with error handling. In this section, we will look at the anyhow
crate and the thiserror
crate. This is not an exhaustive list of error-handling crates, but it will give you an idea of what is out there.
Of course, we can not go to deep into these crates. If you want to learn more about these crates, you can check out the links at the end of this section.
The thiserror
crate
thiserror
provides a derived implementation which adds the error
trait for us. This makes it easier to implement the error
trait for our custom error types.
To use the thiserror
crate, we have to add the crate to our Cargo.toml
file. The cargo add
command will add the thiserror
crate to our Cargo.toml
file.
cargo add thiserror
We can now use the thiserror
crate in our code. We will create a custom error type for our read_username_from_file
function called CustomError
.
use std::error::Error;
use std::fs::File;
use std::io::Read;
#[derive(Debug, thiserror::Error)]
enum CustomError {
#[error("OMG! There is an error {0}")]
BadError(#[from] std::io::Error),
}
fn read_username_from_file() -> Result<String, CustomError> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
The anyhow
crate
anyhow
provides an idiomatic alternative to explicitly handling errors. It is similar to the previously mentioned error
trait but has additional features such as adding context to thrown errors.
To add the anyhow
crate to our project, we can use the cargo add
command.
cargo add anyhow
We can now use the anyhow
crate in our code. We will create a custom error type for our read_username_from_file
function called CustomError
.
use std::fs::File;
use std::io::Read;
use anyhow::Context;
fn read_username_from_file() -> Result<String, anyhow::Error> {
let mut username = String::new();
File::open("hello.txt").context("Failed to open file")?.read_to_string(&mut username).context("Failed to read file")?;
Ok(username)
}
When to use thiserror
and anyhow
The thiserror
crate is useful when you want to implement the Error
trait for your custom error types. The anyhow
crate is useful when you don’t care about the error type and just want to add context to the error.
Summary
In this article, we looked at error handling in Rust
🦀. We talked about non-recoverable errors and recoverable errors. The error handling in Rust
🦀 is designed to help you in writing code that is more robust and less error-prone. The panic!
macro is used for non-recoverable errors when your program is in a state where it can not continue and should stop instead of trying to proceed with invalid or incorrect data. The Result
type is used for recoverable errors. The Result
enums indicate that the operation can fail and that our code can recover from the error and the caller of the piece of code has to handle the success or failure of the operation.
Resources
-
Error Handling
-
The
anyhow
crate -
The
thiserror
crate