New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and
privacy statement. We’ll occasionally send you account related emails.
Already on GitHub?
Sign in
to your account
Closed
cnmade opened this issue
Mar 29, 2016
· 4 comments
Closed
404 not found while install rust lang
#32566
cnmade opened this issue
Mar 29, 2016
· 4 comments
Comments
https://static.rust-lang.org/dist/channel-rust-stable.toml.sha256
was 404 not found while we try to install rust lang .
huzhifeng@w ~ $ wget -qO- https://static.rust-lang.org/rustup.sh | sh
rustup: gpg available. signatures will be verified
rustup: downloading manifest for 'stable'
rustup: command failed: curl -s -f -O https://static.rust-lang.org/dist/channel-rust-stable.toml.sha256
rustup: couldn't download checksum file 'https://static.rust-lang.org/dist/channel-rust-stable.toml.sha256'
rustup: downloading manifest for 'stable'
This always happens for stable, but the installation should continue anyway.
@durka Oddly enough beta.toml.sha256 is in https://static.rust-lang.org/dist/
@netroby Have you tried https://www.rustup.rs/ ? It can install the toolchains under your $HOME and keep them updated with the latest releases. The bad news is it’s still at an early stage of development. The good news is, the compiler itself is quite stable unlike the installer
Newcomers will keep posting the same bug again and again unless the team really fixes it. And how can they trust a compiler when they encounter an error even before they run it?
@netroby Hello!
This seems to affect the checksum file only.
Rust compiler will install fine at your end.
In the meantime, I’m going to explore this further.
Thanks for reporting!
Rust lang 1.8.0 rustup seems fix this bug now.
I started doing university lectures on Rust, as well as holding workshops and trainings. One of the parts that evolved from a couple of slides into a full-blown session was everything around error handling in Rust, since it’s so incredibly good!
Not only does it help making impossible states impossible, but there’s also so much detail to it that handling errors – much like everything in Rust – becomes very ergonomic and easy to read and use.
Making impossible states impossible #
In Rust, there are no things like undefined
or null
, nor do you have exceptions like you know it from programming languages like Java or C#. Instead, you use built-in enums to model state:
Option<T>
for bindings that might possibly have no value (e.g.Some(x)
orNone
)Result<T, E>
for results from operations that might error (e.g.Ok(val)
vsErr(error)
)
The difference between the two is very nuanced and depends a lot on the semantics of your code. The way both enums work is very similar though. The most important thing, in my opinion, is that both types request from you to deal with them. Either by explicitly handling all states, or by explicitly ignoring them.
In this article, I want to focus on Result<T, E>
as this one actually contains errors.
Result<T, E>
is an enum with two variants:
enum Result<T, E> {
Ok(T),
Err(E),
}
T
, E
are generics. T
can be any value, E
can be any error. The two variants Ok
and Err
are globally available.
Use Result<T, E>
when you have things that might go wrong. An operation that is expected to succeed, but there might be cases where it doesn’t. Once you have a Result
value, you can do the following:
- Deal with the states!
- Ignore it
- Panic!
- Use fallbacks
- Propagate errors
Let’s see what I mean in detail.
Deal with the error state #
Let’s write a little piece where we want to read a string from a file. It requires us to
- Read a file
- Read a string from this file
Both operations might cause a std::io::Error
because something unforeseen can happen (the file doesn’t exist, or it can’t be read from, etc.). So the function we’re writing can return either a String
or an io::Error
.
use std::io;
use std::fs::File;fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let f = File::open(path);
/* 1 */
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
/* 2 */
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(err) => Err(err),
}
}
This is what happens:
- When we open a file from
path
, it either can return a filehandle to work withOk(file)
, or it causes an errorErr(e)
. Withmatch f
we’re forced to deal with the two possible states. Either we assign the filehandle tof
(notice the shadowing off
), or we return from the function by returning the error. Thereturn
statement here is important as we want to exit the function. - We then want to read the contents into
s
, the string we just created. It again can either succeed or throw an error. The functionf.read_to_string
returns the length of bytes read, so we can safely ignore the value and return anOk(s)
with the string read. In the other case, we just return the same error. Note that I didn’t write a semi-colon at the end of thematch
expression. Since it’s an expression, this is what we return from the function at this point.
This might look very verbose (it is…), but you see two very important aspects of error handling:
- In both cases you’re expected to deal with the two possible states. You can’t continue if don’t do something
- Features like shadowing (binding a value to an existing name) and expressions make even verbose code easy to read and use
The operation we just did is often called unwrapping. Because you unwrap the value that is wrapped inside the enum.
Speaking of unwrapping…
Ignore the errors #
If you’re very confident that your program won’t fail, you can simply .unwrap()
your values using the built-in functions:
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).unwrap(); /* 1 */
let mut s = String::new();
f.read_to_string(&mut s).unwrap(); /* 1 */
Ok(s) /* 2 */
}
Here’s what happens:
- In all cases that might cause an error, we’re calling
unwrap()
to get to the value - We wrap the result in an
Ok
variant which we return. We could just returns
and drop theResult<T, E>
in our function signature. We keep it because we use it in the other examples again.
The unwrap()
function itself is very much like what we did in the first step where we dealt with all states:
// result.rsimpl<T, E: fmt::Debug> Result<T, E> {
// ...
pub fn unwrap(&self) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
}
}
// ...
}
unwrap_failed
is a shortcut to the panic!
macro. This means if you use .unwrap()
and you don’t have a successful result, your software crashes. 😱
You might ask yourself: How is this different from errors that just crash the software in other programming languages? The answer is easy: You have to be explicit about it. Rust requires you to do something, even if it’s explicitly allowing to panic.
There are lots of different .unwrap_
functions you can use for various situations. We look at one or two of them further on.
Panic! #
Speaking of panics, you can also panic with your own panic message:
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).expect("Error opening file");
let mut s = String::new();
f.read_to_string(&mut s).expect("Error reading file to string");
Ok(s)
}
What .expect(...)
does is very similar to unwrap()
impl<T, E: fmt::Debug> Result<T, E> {
// ...
pub fn expect(self, msg: &str) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed(msg, &e),
}
}
}
But, you have your panic messages in your hand, which you might like!
But even if we are explicit at all times, we may want our software not to panic and crash whenever we encounter an error state. We might want to do something useful, like providing fallbacks or … well … actually handling errors.
Fallback values #
Rust has the possibility to use default values on their Result
(and Option
) enums.
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let s = fs::read_to_string(path).unwrap_or("admin".to_string());
Ok(s)
}
"admin"
might not be the best fallback for a username, but you get the idea. Instead of crashing, we return a default value in the case of an error result. The method.unwrap_or_else
takes a closure for more complex default values.fs::read_to_string
is a shortcut fromstd::fs
for exactly what we did above.
That’s better! Still, what we’ve learned so far is a trade-off between being very verbose, or allowing for explicit crashes, or maybe having fallback values. But can we have both? Concise code and error safety? We can!
Propagate the error #
One of the features I love most with Rust’s Result
types is the possibility to propagate an error. Both functions that might cause an error have the same error type: io::Error
. We can use the question mark operator after each operation to write code for the happy path (only success results), and return error results if something goes wrong:
fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
In this piece, f
is a file handler, f.read_to_string
saves to s
. If anything goes wrong, we return from the function with Err(io::Error)
. Concise code, but we deal with the error one level above:
fn main() {
match read_username_from_file("user.txt") {
Ok(username) => println!("Welcome {}", username),
Err(err) => eprintln!("Whoopsie! {}", err)
};
}
The great thing about it?
- We are still explicit, we have to do something! You can still find all the spots where errors can happen!
- We can write concise code as if errors wouldn’t exist. Errors still have to be dealt with! Either from us or from the users of our function.
The question mark operator also works on Option<T>
, this also allows for some really nice and elegant code!
Propagating different errors #
The problem is though, that methods like this only work when the error types are the same. If we have two different types of errors, we have to get creative. Look at this slightly modified function, where we open and read files, but then parse the read content into a u64
fn read_number_from_file(filename: &str) -> Result<u64, ???> {
let mut file = File::open(filename)?; /* 1 */ let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */
let parsed: u64 = buffer.trim().parse()?; /* 2 */
Ok(parsed)
}
- These two spots can cause
io::Error
, as we know from the previous examples - This operation however can cause a
ParseIntError
The problem is, we don’t know which error we get at compile time. This is entirely up to our code running. We could handle each error through match
expressions and return our own error type. Which is valid, but makes our code verbose again. Or we prepare for “things that happen at runtime”!
Check out our slightly changed function
use std::error;
fn read_number_from_file(filename: &str) -> Result<u64, Box<dyn error::Error>> {
let mut file = File::open(filename)?; /* 1 */
let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */
let parsed: u64 = buffer.trim().parse()?; /* 2 */
Ok(parsed)
}
This is what happens:
- Instead of returning an error implementation, we tell Rust that something that implements the
Error
error trait is coming along. - Since we don’t know what this can be at compile-time, we have to make it a trait object:
dyn std::error::Error
. - And since we don’t know how big this will be, we wrap it in a
Box
. A smart pointer that points to data that will be eventually on the heap
A Box<dyn Trait>
enables dynamic dispatch in Rust: The possibility to dynamically call a function that is not known at compile time. For that, Rust introduces a vtable that keeps pointers to the actual implementations. At runtime, we use these pointers to invoke the appropriate function implementations.
And now, our code is concise again, and our users have to deal with the eventual error.
The first question I get when I show this to folks in my courses is: But can we eventually check which type of error has happened? We can! The downcast_ref()
method allows us to get back to the original type.
fn main() {
match read_number_from_file("number.txt") {
Ok(v) => println!("Your number is {}", v),
Err(err) => {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
eprintln!("Error during IO! {}", io_err)
} else if let Some(pars_err) = err.downcast_ref::<ParseIntError>() {
eprintln!("Error during parsing {}", pars_err)
}
}
};
}
Groovy!
Custom errors #
It’s getting even better and more flexible if you want to create custom errors for your operations. To use custom errors, your error structs have to implement the std::error::Error
trait. This can be a classic struct, a tuple struct or even a unit struct.
You don’t have to implement any functions of std::error::Error
, but you need to implement both the Debug
and the Display
trait. The reasoning is that errors want to be printed somewhere. Here’s how an example looks like:
#[derive(Debug)] /* 1 */
pub struct ParseArgumentsError(String); /* 2 */impl std::error::Error for ParseArgumentsError {} /* 3 */
/* 4 */
impl Display for ParseArgumentsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
- We derive the
Debug
trait. - Our
ParseArgumentsError
is a tuple struct with one element: A custom message - We implement
std::error::Error
forParseArgumentsError
. No need to implement anything else - We implement
Display
, where we print out the single element of our tuple.
And that’s it!
Anyhow… #
Since a lot of the things you just learned a very common, there are of course crates available that abstract most of it. The fantastic anyhow crate is one of them and gives you trait object-based error handling with convenience macros and types.
Bottom line #
This is a very quick primer on error handling in Rust. There is of course more to it, but it should get you started! This is also my first technical Rust article, and I hope many more are coming. Let me know if you liked it and if you find any … haha … errors (ba-dum-ts 🥁), I’m just a tweet away.
Loading
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
- 8550 words
- 43 min
This article is a sample from Zero To Production In Rust, a hands-on introduction to backend development in Rust.
You can get a copy of the book at zero2prod.com.
TL;DR
To send a confirmation email you have to stitch together multiple operations: validation of user input, email dispatch, various database queries.
They all have one thing in common: they may fail.
In Chapter 6 we discussed the building blocks of error handling in Rust — Result
and the ?
operator.
We left many questions unanswered: how do errors fit within the broader architecture of our application? What does a good error look like? Who are errors for? Should we use a library? Which one?
An in-depth analysis of error handling patterns in Rust will be the sole focus of this chapter.
Chapter 8
- What Is The Purpose Of Errors?
- Internal Errors
- Enable The Caller To React
- Help An Operator To Troubleshoot
- Errors At The Edge
- Help A User To Troubleshoot
- Summary
- Internal Errors
- Error Reporting For Operators
- Keeping Track Of The Error Root Cause
- The
Error
Trait- Trait Objects
Error::source
- Errors For Control Flow
- Layering
- Modelling Errors as Enums
- The Error Type Is Not Enough
- Removing The Boilerplate With
thiserror
- Avoid «Ball Of Mud» Error Enums
- Using
anyhow
As Opaque Error Type anyhow
Orthiserror
?
- Using
- Who Should Log Errors?
- Summary
What Is The Purpose Of Errors?
Let’s start with an example:
//! src/routes/subscriptions.rs
// [...]
pub async fn store_token(
transaction: &mut Transaction<'_, Postgres>,
subscriber_id: Uuid,
subscription_token: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO subscription_tokens (subscription_token, subscriber_id)
VALUES ($1, $2)
"#,
subscription_token,
subscriber_id
)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
Ok(())
}
We are trying to insert a row into the subscription_tokens
table in order to store a newly-generated token against a subscriber_id
.
execute
is a fallible operation: we might have a network issue while talking to the database, the row we are trying to insert might violate some table constraints (e.g. uniqueness of the primary key), etc.
Internal Errors
Enable The Caller To React
The caller of execute
most likely wants to be informed if a failure occurs — they need to react accordingly, e.g. retry the query or propagate the failure upstream using ?
, as in our example.
Rust leverages the type system to communicate that an operation may not succeed: the return type of execute
is Result
, an enum.
pub enum Result<Success, Error> {
Ok(Success),
Err(Error)
}
The caller is then forced by the compiler to express how they plan to handle both scenarios — success and failure.
If our only goal was to communicate to the caller that an error happened, we could use a simpler definition for Result
:
pub enum ResultSignal<Success> {
Ok(Success),
Err
}
There would be no need for a generic Error
type — we could just check that execute
returned the Err
variant, e.g.
let outcome = sqlx::query!(/* ... */)
.execute(transaction)
.await;
if outcome == ResultSignal::Err {
// Do something if it failed
}
This works if there is only one failure mode.
Truth is, operations can fail in multiple ways and we might want to react differently depending on what happened.
Let’s look at the skeleton of sqlx::Error
, the error type for execute
:
//! sqlx-core/src/error.rs
pub enum Error {
Configuration(/* */),
Database(/* */),
Io(/* */),
Tls(/* */),
Protocol(/* */),
RowNotFound,
TypeNotFound {/* */},
ColumnIndexOutOfBounds {/* */},
ColumnNotFound(/* */),
ColumnDecode {/* */},
Decode(/* */),
PoolTimedOut,
PoolClosed,
WorkerCrashed,
Migrate(/* */),
}
Quite a list, ain’t it?
sqlx::Error
is implemented as an enum to allow users to match on the returned error and behave differently depending on the underlying failure mode. For example, you might want to retry a PoolTimedOut
while you will probably give up on a ColumnNotFound
.
Help An Operator To Troubleshoot
What if an operation has a single failure mode — should we just use ()
as error type?
Err(())
might be enough for the caller to determine what to do — e.g. return a 500 Internal Server Error
to the user.
But control flow is not the only purpose of errors in an application.
We expect errors to carry enough context about the failure to produce a report for an operator (e.g. the developer) that contains enough details to go and troubleshoot the issue.
What do we mean by report?
In a backend API like ours it will usually be a log event.
In a CLI it could be an error message shown in the terminal when a --verbose
flag is used.
The implementation details may vary, the purpose stays the same: help a human understand what is going wrong.
That’s exactly what we are doing in the initial code snippet:
//! src/routes/subscriptions.rs
// [...]
pub async fn store_token(/* */) -> Result<(), sqlx::Error> {
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
// [...]
}
If the query fails, we grab the error and emit a log event. We can then go and inspect the error logs when investigating the database issue.
Errors At The Edge
Help A User To Troubleshoot
So far we focused on the internals of our API — functions calling other functions and operators trying to make sense of the mess after it happened.
What about users?
Just like operators, users expect the API to signal when a failure mode is encountered.
What does a user of our API see when store_token
fails?
We can find out by looking at the request handler:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> HttpResponse {
// [...]
if store_token(&mut transaction, subscriber_id, &subscription_token)
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
// [...]
}
They receive an HTTP response with no body and a 500 Internal Server Error
status code.
The status code fulfills the same purpose of the error type in store_token
: it is a machine-parsable piece of information that the caller (e.g. the browser) can use to determine what to do next (e.g. retry the request assuming it’s a transient failure).
What about the human behind the browser? What are we telling them?
Not much, the response body is empty.
That is actually a good implementation: the user should not have to care about the internals of the API they are calling — they have no mental model of it and no way to determine why it is failing. That’s the realm of the operator.
We are omitting those details by design.
In other circumstances, instead, we need to convey additional information to the human user. Let’s look at our input validation for the same endpoint:
//! src/routes/subscriptions.rs
#[derive(serde::Deserialize)]
pub struct FormData {
email: String,
name: String,
}
impl TryFrom<FormData> for NewSubscriber {
type Error = String;
fn try_from(value: FormData) -> Result<Self, Self::Error> {
let name = SubscriberName::parse(value.name)?;
let email = SubscriberEmail::parse(value.email)?;
Ok(Self { email, name })
}
}
We received an email address and a name as data attached to the form submitted by the user. Both fields are going through an additional round of validation — SubscriberName::parse
and SubscriberEmail::parse
. Those two methods are fallible — they return a String
as error type to explain what has gone wrong:
//! src/domain/subscriber_email.rs
// [...]
impl SubscriberEmail {
pub fn parse(s: String) -> Result<SubscriberEmail, String> {
if validate_email(&s) {
Ok(Self(s))
} else {
Err(format!("{} is not a valid subscriber email.", s))
}
}
}
It is, I must admit, not the most useful error message: we are telling the user that the email address they entered is wrong, but we are not helping them to determine why.
In the end, it doesn’t matter: we are not sending any of that information to the user as part of the response of the API — they are getting a 400 Bad Request
with no body.
//! src/routes/subscription.rs
// [...]
pub async fn subscribe(/* */) -> HttpResponse {
let new_subscriber = match form.0.try_into() {
Ok(form) => form,
Err(_) => return HttpResponse::BadRequest().finish(),
};
// [...]
This is a poor error: the user is left in the dark and cannot adapt their behaviour as required.
Summary
Let’s summarise what we uncovered so far.
Errors serve two1 main purposes:
- Control flow (i.e. determine what do next);
- Reporting (e.g. investigate, after the fact, what went wrong on).
We can also distinguish errors based on their location:
- Internal (i.e. a function calling another function within our application);
- At the edge (i.e. an API request that we failed to fulfill).
Control flow is scripted: all information required to take a decision on what to do next must be accessible to a machine.
We use types (e.g. enum variants), methods and fields for internal errors.
We rely on status codes for errors at the edge.
Error reports, instead, are primarily consumed by humans.
The content has to be tuned depending on the audience.
An operator has access to the internals of the system — they should be provided with as much context as possible on the failure mode.
A user sits outside the boundary of the application2: they should only be given the amount of information required to adjust their behaviour if necessary (e.g. fix malformed inputs).
We can visualise this mental model using a 2×2 table with Location
as columns and Purpose
as rows:
Internal | At the edge | |
---|---|---|
Control Flow | Types, methods, fields | Status codes |
Reporting | Logs/traces | Response body |
We will spend the rest of the chapter improving our error handling strategy for each of the cells in the table.
Error Reporting For Operators
Let’s start with error reporting for operators.
Are we doing a good job with logging right now when it comes to errors?
Let’s write a quick test to find out:
//! tests/api/subscriptions.rs
// [...]
#[tokio::test]
async fn subscribe_fails_if_there_is_a_fatal_database_error() {
// Arrange
let app = spawn_app().await;
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com";
// Sabotage the database
sqlx::query!("ALTER TABLE subscription_tokens DROP COLUMN subscription_token;",)
.execute(&app.db_pool)
.await
.unwrap();
// Act
let response = app.post_subscriptions(body.into()).await;
// Assert
assert_eq!(response.status().as_u16(), 500);
}
The test passes straight away — let’s look at the log emitted by the application3.
Make sure you are running on
tracing-actix-web
0.4.0-beta.8
,tracing-bunyan-formatter
0.2.4
andactix-web 4.0.0-beta.8
!
# sqlx logs are a bit spammy, cutting them out to reduce noise
export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
The output, once you focus on what matters, is the following:
INFO: [HTTP REQUEST - START]
INFO: [ADDING A NEW SUBSCRIBER - START]
INFO: [SAVING NEW SUBSCRIBER DETAILS IN THE DATABASE - START]
INFO: [SAVING NEW SUBSCRIBER DETAILS IN THE DATABASE - END]
INFO: [STORE SUBSCRIPTION TOKEN IN THE DATABASE - START]
ERROR: [STORE SUBSCRIPTION TOKEN IN THE DATABASE - EVENT] Failed to execute query:
Database(PgDatabaseError {
severity: Error,
code: "42703",
message:
"column 'subscription_token' of relation
'subscription_tokens' does not exist",
...
})
target=zero2prod::routes::subscriptions
INFO: [STORE SUBSCRIPTION TOKEN IN THE DATABASE - END]
INFO: [ADDING A NEW SUBSCRIBER - END]
ERROR: [HTTP REQUEST - EVENT] Error encountered while
processing the incoming HTTP request: ""
exception.details="",
exception.message="",
target=tracing_actix_web::middleware
INFO: [HTTP REQUEST - END]
exception.details="",
exception.message="",
target=tracing_actix_web::root_span_builder,
http.status_code=500
How do you read something like this?
Ideally, you start from the outcome: the log record emitted at the end of request processing. In our case, that is:
INFO: [HTTP REQUEST - END]
exception.details="",
exception.message="",
target=tracing_actix_web::root_span_builder,
http.status_code=500
What does that tell us?
The request returned a 500
status code — it failed.
We don’t learn a lot more than that: both exception.details
and exception.message
are empty.
The situation does not get much better if we look at the next log, emitted by tracing_actix_web
:
ERROR: [HTTP REQUEST - EVENT] Error encountered while
processing the incoming HTTP request: ""
exception.details="",
exception.message="",
target=tracing_actix_web::middleware
No actionable information whatsoever. Logging «Oops! Something went wrong!» would have been just as useful.
We need to keep looking, all the way to the last remaining error log:
ERROR: [STORE SUBSCRIPTION TOKEN IN THE DATABASE - EVENT] Failed to execute query:
Database(PgDatabaseError {
severity: Error,
code: "42703",
message:
"column 'subscription_token' of relation
'subscription_tokens' does not exist",
...
})
target=zero2prod::routes::subscriptions
Something went wrong when we tried talking to the database — we were expecting to see a subscription_token
column in the subscription_tokens
table but, for some reason, it was not there.
This is actually useful!
Is it the cause of the 500
though?
Difficult to say just by looking at the logs — a developer will have to clone the codebase, check where that log line is coming from and make sure that it’s indeed the cause of the issue.
It can be done, but it takes time: it would be much easier if the [HTTP REQUEST - END]
log record reported something useful about the underlying root cause in exception.details
and exception.message
.
Keeping Track Of The Error Root Cause
To understand why the log records coming out tracing_actix_web
are so poor we need to inspect (again) our request handler and store_token
:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> HttpResponse {
// [...]
if store_token(&mut transaction, subscriber_id, &subscription_token)
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
// [...]
}
pub async fn store_token(/* */) -> Result<(), sqlx::Error> {
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
// [...]
}
The useful error log we found is indeed the one emitted by that tracing::error
call — the error message includes the sqlx::Error
returned by execute
.
We propagate the error upwards using the ?
operator, but the chain breaks in subscribe
— we discard the error we received from store_token
and build a bare 500
response.
HttpResponse::InternalServerError().finish()
is the only thing that actix_web
and tracing_actix_web::TracingLogger
get to access when they are about to emit their respective log records. The error does not contain any context about the underlying root cause, therefore the log records are equally useless.
How do we fix it?
We need to start leveraging the error handling machinery exposed by actix_web
— in particular, actix_web::Error
.
According to the documentation:
actix_web::Error
is used to carry errors fromstd::error
throughactix_web
in a convenient way.
It sounds exactly like what we are looking for.
How do we build an instance of actix_web::Error
?
The documentation states that
actix_web::Error
can be created by converting errors withinto()
.
A bit indirect, but we can figure it out4.
The only From
/Into
implementation that we can use, browsing the ones listed in the documentation, seems to be this one:
/// Build an `actix_web::Error` from any error that implements `ResponseError`
impl<T: ResponseError + 'static> From<T> for Error {
fn from(err: T) -> Error {
Error {
cause: Box::new(err),
}
}
}
ResponseError
is a trait exposed by actix_web
:
/// Errors that can be converted to `Response`.
pub trait ResponseError: fmt::Debug + fmt::Display {
/// Response's status code.
///
/// The default implementation returns an internal server error.
fn status_code(&self) -> StatusCode;
/// Create a response from the error.
///
/// The default implementation returns an internal server error.
fn error_response(&self) -> Response;
}
We just need to implement it for our errors!
actix_web
provides a default implementation for both methods that returns a 500 Internal Server Error
— exactly what we need. Therefore it’s enough to write:
//! src/routes/subscriptions.rs
use actix_web::ResponseError;
// [...]
impl ResponseError for sqlx::Error {}
The compiler is not happy:
error[E0117]: only traits defined in the current crate
can be implemented for arbitrary types
--> src/routes/subscriptions.rs:162:1
|
162 | impl ResponseError for sqlx::Error {}
| ^^^^^^^^^^^^^^^^^^^^^^^-----------
| | |
| | `sqlx::Error` is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
We just bumped into Rust’s orphan rule: it is forbidden to implement a foreign trait for a foreign type, where foreign stands for «from another crate».
This restriction is meant to preserve coherence: imagine if you added a dependency that defined its own implementation of ResponseError
for sqlx::Error
— which one should the compiler use when the trait methods are invoked?
Orphan rule aside, it would still be a mistake for us to implement ResponseError
for sqlx::Error
.
We want to return a 500 Internal Server Error
when we run into a sqlx::Error
while trying to persist a subscriber token.
In another circumstance we might wish to handle a sqlx::Error
differently.
We should follow the compiler’s suggestion: define a new type to wrap sqlx::Error
.
//! src/routes/subscriptions.rs
// [...]
// Using the new error type!
pub async fn store_token(/* */) -> Result<(), StoreTokenError> {
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
// [...]
// Wrapping the underlying error
StoreTokenError(e)
})?;
// [...]
}
// A new error type, wrapping a sqlx::Error
pub struct StoreTokenError(sqlx::Error);
impl ResponseError for StoreTokenError {}
It doesn’t work, but for a different reason:
error[E0277]: `StoreTokenError` doesn't implement `std::fmt::Display`
--> src/routes/subscriptions.rs:164:6
|
164 | impl ResponseError for StoreTokenError {}
| ^^^^^^^^^^^^^
`StoreTokenError` cannot be formatted with the default formatter
|
|
59 | pub trait ResponseError: fmt::Debug + fmt::Display {
| ------------
| required by this bound in `ResponseError`
|
= help: the trait `std::fmt::Display` is not implemented for `StoreTokenError`
error[E0277]: `StoreTokenError` doesn't implement `std::fmt::Debug`
--> src/routes/subscriptions.rs:164:6
|
164 | impl ResponseError for StoreTokenError {}
| ^^^^^^^^^^^^^
`StoreTokenError` cannot be formatted using `{:?}`
|
|
59 | pub trait ResponseError: fmt::Debug + fmt::Display {
| ----------
required by this bound in `ResponseError`
|
= help: the trait `std::fmt::Debug` is not implemented for `StoreTokenError`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
We are missing two trait implementations on StoreTokenError
: Debug
and Display
.
Both traits are concerned with formatting, but they serve a different purpose.
Debug
should return a programmer-facing representation, as faithful as possible to the underlying type structure, to help with debugging (as the name implies). Almost all public types should implement Debug
.
Display
, instead, should return a user-facing representation of the underlying type. Most types do not implement Display
and it cannot be automatically implemented with a #[derive(Display)]
attribute.
When working with errors, we can reason about the two traits as follows: Debug
returns as much information as possible while Display
gives us a brief description of the failure we encountered, with the essential amount of context.
Let’s give it a go for StoreTokenError
:
//! src/routes/subscriptions.rs
// [...]
// We derive `Debug`, easy and painless.
#[derive(Debug)]
pub struct StoreTokenError(sqlx::Error);
impl std::fmt::Display for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"A database error was encountered while \
trying to store a subscription token."
)
}
}
It compiles!
We can now leverage it in our request handler:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, actix_web::Error> {
// You will have to wrap (early) returns in `Ok(...)` as well!
// [...]
// The `?` operator transparently invokes the `Into` trait
// on our behalf - we don't need an explicit `map_err` anymore.
store_token(/* */).await?;
// [...]
}
Let’s look at our logs again:
# sqlx logs are a bit spammy, cutting them out to reduce noise
export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
...
INFO: [HTTP REQUEST - END]
exception.details= StoreTokenError(
Database(
PgDatabaseError {
severity: Error,
code: "42703",
message:
"column 'subscription_token' of relation
'subscription_tokens' does not exist",
...
}
)
)
exception.message=
"A database failure was encountered while
trying to store a subscription token.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
Much better!
The log record emitted at the end of request processing now contains both an in-depth and brief description of the error that caused the application to return a 500 Internal Server Error
to the user.
It is enough to look at this log record to get a pretty accurate picture of everything that matters for this request.
The Error
Trait
So far we moved forward by following the compiler suggestions, trying to satisfy the constraints imposed on us by actix-web
when it comes to error handling.
Let’s step back to look at the bigger picture: what should an error look like in Rust (not considering the specifics of actix-web
)?
Rust’s standard library has a dedicated trait, Error
.
pub trait Error: Debug + Display {
/// The lower-level source of this error, if any.
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
It requires an implementation of Debug
and Display
, just like ResponseError
.
It also gives us the option to implement a source
method that returns the underlying cause of the error, if any.
What is the point of implementing the Error
trait at all for our error type?
It is not required by Result
— any type can be used as error variant there.
pub enum Result<T, E> {
/// Contains the success value
Ok(T),
/// Contains the error value
Err(E),
}
The Error
trait is, first and foremost, a way to semantically mark our type as being an error. It helps a reader of our codebase to immediately spot its purpose.
It is also a way for the Rust community to standardise on the minimum requirements for a good error:
- it should provide different representations (
Debug
andDisplay
), tuned to different audiences; - it should be possible to look at the underlying cause of the error, if any (
source
).
This list is still evolving — e.g. there is an unstable backtrace
method.
Error handling is an active area of research in the Rust community — if you are interested in staying on top of what is coming next I strongly suggest you to keep an eye on the Rust Error Handling Working Group.
By providing a good implementation of all the optional methods we can fully leverage the error handling ecosystem — functions that have been designed to work with errors, generically. We will be writing one in a couple of sections!
Trait Objects
Before we work on implementing source
, let’s take a closer look at its return — Option<&(dyn Error + 'static)>
.
dyn Error
is a trait object5 — a type that we know nothing about apart from the fact that it implements the Error
trait.
Trait objects, just like generic type parameters, are a way to achieve polymorphism in Rust: invoke different implementations of the same interface. Generic types are resolved at compile-time (static dispatch), trait objects incur a runtime cost (dynamic dispatch).
Why does the standard library return a trait object?
It gives developers a way to access the underlying root cause of current error while keeping it opaque.
It does not leak any information about the type of the underlying root cause — you only get access to the methods exposed by the Error
trait6: different representations (Debug
, Display
), the chance to go one level deeper in the error chain using source
.
Error::source
Let’s implement Error
for StoreTokenError
:
//! src/routes/subscriptions.rs
// [..]
impl std::error::Error for StoreTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// The compiler transparently casts `&sqlx::Error` into a `&dyn Error`
Some(&self.0)
}
}
source
is useful when writing code that needs to handle a variety of errors: it provides a structured way to navigate the error chain without having to know anything about the specific error type you are working with.
If we look at our log record, the causal relationship between StoreTokenError
and sqlx::Error
is somewhat implicit — we infer one is the cause of the other because it is a part of it.
...
INFO: [HTTP REQUEST - END]
exception.details= StoreTokenError(
Database(
PgDatabaseError {
severity: Error,
code: "42703",
message:
"column 'subscription_token' of relation
'subscription_tokens' does not exist",
...
}
)
)
exception.message=
"A database failure was encountered while
trying to store a subscription token.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
Let’s go for something more explicit:
//! src/routes/subscriptions.rs
// Notice that we have removed `#[derive(Debug)]`
pub struct StoreTokenError(sqlx::Error);
impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\nCaused by:\n\t{}", self, self.0)
}
}
The log record leaves nothing to the imagination now:
...
INFO: [HTTP REQUEST - END]
exception.details=
"A database failure was encountered
while trying to store a subscription token.
Caused by:
error returned from database: column 'subscription_token'
of relation 'subscription_tokens' does not exist"
exception.message=
"A database failure was encountered while
trying to store a subscription token.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
exception.details
is easier to read and still conveys all the relevant information we had there before.
Using source
we can write a function that provides a similar representation for any type that implements Error
:
//! src/routes/subscriptions.rs
// [...]
fn error_chain_fmt(
e: &impl std::error::Error,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
writeln!(f, "{}\n", e)?;
let mut current = e.source();
while let Some(cause) = current {
writeln!(f, "Caused by:\n\t{}", cause)?;
current = cause.source();
}
Ok(())
}
It iterates over the whole chain of errors7 that led to the failure we are trying to print.
We can then change our implementation of Debug
for StoreTokenError
to use it:
//! src/routes/subscriptions.rs
// [...]
impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
The result is identical — and we can reuse it when working with other errors if we want a similar Debug
representation.
Errors For Control Flow
Layering
We achieved the outcome we wanted (useful logs), but I am not too fond of the solution: we implemented a trait from our web framework (ResponseError
) for an error type returned by an operation that is blissfully unaware of REST or the HTTP protocol, store_token
. We could be calling store_token
from a different entrypoint (e.g. a CLI) — nothing should have to change in its implementation.
Even assuming we are only ever going to be invoking store_token
in the context of a REST API, we might add other endpoints that rely on that routine — they might not want to return a 500
when it fails.
Choosing the appropriate HTTP status code when an error occurs is a concern of the request handler, it should not leak elsewhere.
Let’s delete
//! src/routes/subscriptions.rs
// [...]
// Nuke it!
impl ResponseError for StoreTokenError {}
To enforce a proper separation of concerns we need to introduce another error type, SubscribeError
. We will use it as failure variant for subscribe
and it will own the HTTP-related logic (ResponseError
‘s implementation).
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
}
#[derive(Debug)]
struct SubscribeError {}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Failed to create a new subscriber."
)
}
}
impl std::error::Error for SubscribeError {}
impl ResponseError for SubscribeError {}
If you run cargo check
you will see an avalanche of '?' couldn't convert the error to 'SubscribeError'
— we need to implement conversions from the error types returned by our functions and SubscribeError
.
Modelling Errors as Enums
An enum is the most common approach to work around this issue: a variant for each error type we need to deal with.
//! src/routes/subscriptions.rs
// [...]
#[derive(Debug)]
pub enum SubscribeError {
ValidationError(String),
DatabaseError(sqlx::Error),
StoreTokenError(StoreTokenError),
SendEmailError(reqwest::Error),
}
We can then leverage the ?
operator in our handler by providing a From
implementation for each of wrapped error types:
//! src/routes/subscriptions.rs
// [...]
impl From<reqwest::Error> for SubscribeError {
fn from(e: reqwest::Error) -> Self {
Self::SendEmailError(e)
}
}
impl From<sqlx::Error> for SubscribeError {
fn from(e: sqlx::Error) -> Self {
Self::DatabaseError(e)
}
}
impl From<StoreTokenError> for SubscribeError {
fn from(e: StoreTokenError) -> Self {
Self::StoreTokenError(e)
}
}
impl From<String> for SubscribeError {
fn from(e: String) -> Self {
Self::ValidationError(e)
}
}
We can now clean up our request handler by removing all those match
/ if fallible_function().is_err()
lines:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
let new_subscriber = form.0.try_into()?;
let mut transaction = pool.begin().await?;
let subscriber_id = insert_subscriber(/* */).await?;
let subscription_token = generate_subscription_token();
store_token(/* */).await?;
transaction.commit().await?;
send_confirmation_email(/* */).await?;
Ok(HttpResponse::Ok().finish())
}
The code compiles, but one of our tests is failing:
thread 'subscriptions::subscribe_returns_a_400_when_fields_are_present_but_invalid'
panicked at 'assertion failed: `(left == right)`
left: `400`,
right: `500`: The API did not return a 400 Bad Request when the payload was empty name.'
We are still using the default implementation of ResponseError
— it always returns 500
.
This is where enum
s shine: we can use a match
statement for control flow — we behave differently depending on the failure scenario we are dealing with.
//! src/routes/subscriptions.rs
use actix_web::http::StatusCode;
// [...]
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::DatabaseError(_)
| SubscribeError::StoreTokenError(_)
| SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
The test suite should pass again.
The Error Type Is Not Enough
What about our logs?
Let’s look again:
export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
...
INFO: [HTTP REQUEST - END]
exception.details="StoreTokenError(
A database failure was encountered while trying to
store a subscription token.
Caused by:
error returned from database: column 'subscription_token'
of relation 'subscription_tokens' does not exist)"
exception.message="Failed to create a new subscriber.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
We are still getting a great representation for the underlying StoreTokenError
in exception.details
, but it shows that we are now using the derived Debug
implementation for SubscribeError
. No loss of information though.
The same cannot be said for exception.message
— no matter the failure mode, we always get Failed to create a new subscriber
. Not very useful.
Let’s refine our Debug
and Display
implementations:
//! src/routes/subscriptions.rs
// [...]
// Remember to delete `#[derive(Debug)]`!
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl std::error::Error for SubscribeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
// &str does not implement `Error` - we consider it the root cause
SubscribeError::ValidationError(_) => None,
SubscribeError::DatabaseError(e) => Some(e),
SubscribeError::StoreTokenError(e) => Some(e),
SubscribeError::SendEmailError(e) => Some(e),
}
}
}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubscribeError::ValidationError(e) => write!(f, "{}", e),
// What should we do here?
SubscribeError::DatabaseError(_) => write!(f, "???"),
SubscribeError::StoreTokenError(_) => write!(
f,
"Failed to store the confirmation token for a new subscriber."
),
SubscribeError::SendEmailError(_) => {
write!(f, "Failed to send a confirmation email.")
},
}
}
}
Debug
is easily sorted: we implemented the Error
trait for SubscribeError
, including source
, and we can use again the helper function we wrote earlier for StoreTokenError
.
We have a problem when it comes to Display
— the same DatabaseError
variant is used for errors encountered when:
- acquiring a new Postgres connection from the pool;
- inserting a subscriber in the
subscribers
table; - committing the SQL transaction.
When implementing Display
for SubscribeError
we have no way to distinguish which of those three cases we are dealing with — the underlying error type is not enough.
Let’s disambiguate by using a different enum variant for each operation:
//! src/routes/subscriptions.rs
// [...]
pub enum SubscribeError {
// [...]
// No more `DatabaseError`
PoolError(sqlx::Error),
InsertSubscriberError(sqlx::Error),
TransactionCommitError(sqlx::Error),
}
impl std::error::Error for SubscribeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
// [...]
// No more DatabaseError
SubscribeError::PoolError(e) => Some(e),
SubscribeError::InsertSubscriberError(e) => Some(e),
SubscribeError::TransactionCommitError(e) => Some(e),
// [...]
}
}
}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// [...]
SubscribeError::PoolError(_) => {
write!(f, "Failed to acquire a Postgres connection from the pool")
}
SubscribeError::InsertSubscriberError(_) => {
write!(f, "Failed to insert new subscriber in the database.")
}
SubscribeError::TransactionCommitError(_) => {
write!(
f,
"Failed to commit SQL transaction to store a new subscriber."
)
}
}
}
}
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::PoolError(_)
| SubscribeError::TransactionCommitError(_)
| SubscribeError::InsertSubscriberError(_)
| SubscribeError::StoreTokenError(_)
| SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
DatabaseError
is used in one more place:
//! src/routes/subscriptions.rs
// [..]
impl From<sqlx::Error> for SubscribeError {
fn from(e: sqlx::Error) -> Self {
Self::DatabaseError(e)
}
}
The type alone is not enough to distinguish which of the new variants should be used; we cannot implement From
for sqlx::Error
.
We have to use map_err
to perform the right conversion in each case.
//! src/routes/subscriptions.rs
// [..]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
let mut transaction = pool.begin().await.map_err(SubscribeError::PoolError)?;
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
.map_err(SubscribeError::InsertSubscriberError)?;
// [...]
transaction
.commit()
.await
.map_err(SubscribeError::TransactionCommitError)?;
// [...]
}
The code compiles and exception.message
is useful again:
...
INFO: [HTTP REQUEST - END]
exception.details="Failed to store the confirmation token
for a new subscriber.
Caused by:
A database failure was encountered while trying to store
a subscription token.
Caused by:
error returned from database: column 'subscription_token'
of relation 'subscription_tokens' does not exist"
exception.message="Failed to store the confirmation token for a new subscriber.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
Removing The Boilerplate With thiserror
It took us roughly 90 lines of code to implement SubscribeError
and all the machinery that surrounds it in order to achieve the desired behaviour and get useful diagnostic in our logs.
That is a lot of code, with a ton of boilerplate (e.g. source
‘s or From
implementations).
Can we do better?
Well, I am not sure we can write less code, but we can find a different way out: we can generate all that boilerplate using a macro!
As it happens, there is already a great crate in the ecosystem for this purpose: thiserror
.
Let’s add it to our dependencies:
#! Cargo.toml
[dependencies]
# [...]
thiserror = "1"
It provides a derive macro to generate most of the code we just wrote by hand.
Let’s see it in action:
//! src/routes/subscriptions.rs
// [...]
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error("Failed to acquire a Postgres connection from the pool")]
PoolError(#[source] sqlx::Error),
#[error("Failed to insert new subscriber in the database.")]
InsertSubscriberError(#[source] sqlx::Error),
#[error("Failed to store the confirmation token for a new subscriber.")]
StoreTokenError(#[from] StoreTokenError),
#[error("Failed to commit SQL transaction to store a new subscriber.")]
TransactionCommitError(#[source] sqlx::Error),
#[error("Failed to send a confirmation email.")]
SendEmailError(#[from] reqwest::Error),
}
// We are still using a bespoke implementation of `Debug`
// to get a nice report using the error source chain
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// We no longer have `#[from]` for `ValidationError`, so we need to
// map the error explicitly
let new_subscriber = form.0.try_into().map_err(SubscribeError::ValidationError)?;
// [...]
}
We cut it down to 21 lines — not bad!
Let’s break down what is happening now.
thiserror::Error
is a procedural macro used via a #[derive(/* */)]
attribute.
We have seen and used these before — e.g. #[derive(Debug)]
or #[derive(serde::Serialize)]
.
The macro receives, at compile-time, the definition of SubscribeError
as input and returns another stream of tokens as output — it generates new Rust code, which is then compiled into the final binary.
Within the context of #[derive(thiserror::Error)]
we get access to other attributes to achieve the behaviour we are looking for:
-
#[error(/* */)]
defines theDisplay
representation of the enum variant it is applied to. E.g.Display
will returnFailed to send a confirmation email.
when invoked on an instance ofSubscribeError::SendEmailError
. You can interpolate values in the final representation — e.g. the{0}
in#[error("{0}")]
on top ofValidationError
is referring to the wrappedString
field, mimicking the syntax to access fields on tuple structs (i.e.self.0
). -
#[source]
is used to denote what should be returned as root cause inError::source
; -
#[from]
automatically derives an implementation ofFrom
for the type it has been applied to into the top-level error type (e.g.impl From<StoreTokenError> for SubscribeError {/* */}
). The field annotated with#[from]
is also used as error source, saving us from having to use two annotations on the same field (e.g.#[source] #[from] reqwest::Error
).
I want to call your attention on a small detail: we are not using either #[from]
or #[source]
for the ValidationError
variant. That is because String
does not implement the Error
trait, therefore it cannot be returned in Error::source
— the same limitation we encountered before when implementing Error::source
manually, which led us to return None
in the ValidationError
case.
Avoid «Ball Of Mud» Error Enums
In SubscribeError
we are using enum variants for two purposes:
- Determine the response that should be returned to the caller of our API (
ResponseError
); - Provide relevant diagnostic (
Error::source
,Debug
,Display
).
SubscribeError
, as currently defined, exposes a lot of the implementation details of subscribe
: we have a variant for every fallible function call we make in the request handler!
It is not a strategy that scales very well.
We need to think in terms of abstraction layers: what does a caller of subscribe
need to know?
They should be able to determine what response to return to a user (via ResponseError
). That’s it.
The caller of subscribe
does not understand the intricacies of the subscription flow: they don’t know enough about the domain to behave differently for a SendEmailError
compared to a TransactionCommitError
(by design!). subscribe
should return an error type that speaks at the right level of abstraction.
The ideal error type would look like this:
//! src/routes/subscriptions.rs
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error(/* */)]
UnexpectedError(/* */),
}
ValidationError
maps to a 400 Bad Request
, UnexpectedError
maps to an opaque 500 Internal Server Error
.
What should we store in the UnexpectedError
variant?
We need to map multiple error types into it — sqlx::Error
, StoreTokenError
, reqwest::Error
.
We do not want to expose the implementation details of the fallible routines that get mapped to UnexpectedError
by subscribe
— it must be opaque.
We bumped into a type that fulfills those requirements when looking at the Error
trait from Rust’s standard library: Box<dyn std::error::Error>
8
Let’s give it a go:
//! src/routes/subscriptions.rs
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
// Transparent delegates both `Display`'s and `source`'s implementation
// to the type wrapped by `UnexpectedError`.
#[error(transparent)]
UnexpectedError(#[from] Box<dyn std::error::Error>),
}
We can still generate an accurate response for the caller:
//! src/routes/subscriptions.rs
// [...]
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
We just need to adapt subscribe
to properly convert our errors before using the ?
operator:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
let mut transaction = pool
.begin()
.await
.map_err(|e| SubscribeError::UnexpectedError(Box::new(e)))?;
let subscriber_id = insert_subscriber(/* */)
.await
.map_err(|e| SubscribeError::UnexpectedError(Box::new(e)))?;
// [...]
store_token(/* */)
.await
.map_err(|e| SubscribeError::UnexpectedError(Box::new(e)))?;
transaction
.commit()
.await
.map_err(|e| SubscribeError::UnexpectedError(Box::new(e)))?;
send_confirmation_email(/* */)
.await
.map_err(|e| SubscribeError::UnexpectedError(Box::new(e)))?;
// [...]
}
There is some code repetition, but let it be for now.
The code compiles and our tests pass as expected.
Let’s change the test we have used so far to check the quality of our log messages: let’s trigger a failure in insert_subscriber
instead of store_token
.
//! tests/api/subscriptions.rs
// [...]
#[tokio::test]
async fn subscribe_fails_if_there_is_a_fatal_database_error() {
// [...]
// Break `subscriptions` instead of `subscription_tokens`
sqlx::query!("ALTER TABLE subscriptions DROP COLUMN email;",)
.execute(&app.db_pool)
.await
.unwrap();
// [..]
}
The test passes, but we can see that our logs have regressed:
INFO: [HTTP REQUEST - END]
exception.details:
"error returned from database: column 'email' of
relation 'subscriptions' does not exist"
exception.message:
"error returned from database: column 'email' of
relation 'subscriptions' does not exist"
We do not see a cause chain anymore.
We lost the operator-friendly error message that was previously attached to the InsertSubscriberError
via thiserror
:
//! src/routes/subscriptions.rs
// [...]
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("Failed to insert new subscriber in the database.")]
InsertSubscriberError(#[source] sqlx::Error),
// [...]
}
That is to be expected: we are forwarding the raw error now to Display
(via #[error(transparent)]
), we are not attaching any additional context to it in subscribe
.
We can fix it — let’s add a new String
field to UnexpectedError
to attach contextual information to the opaque error we are storing:
//! src/routes/subscriptions.rs
// [...]
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error("{1}")]
UnexpectedError(#[source] Box<dyn std::error::Error>, String),
}
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
// [...]
// The variant now has two fields, we need an extra `_`
SubscribeError::UnexpectedError(_, _) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
We need to adjust our mapping code in subscribe
accordingly — we will reuse the error descriptions we had before refactoring SubscribeError
:
//! src/routes/subscriptions.rs
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [..]
let mut transaction = pool.begin().await.map_err(|e| {
SubscribeError::UnexpectedError(
Box::new(e),
"Failed to acquire a Postgres connection from the pool".into(),
)
})?;
let subscriber_id = insert_subscriber(/* */)
.await
.map_err(|e| {
SubscribeError::UnexpectedError(
Box::new(e),
"Failed to insert new subscriber in the database.".into(),
)
})?;
// [..]
store_token(/* */)
.await
.map_err(|e| {
SubscribeError::UnexpectedError(
Box::new(e),
"Failed to store the confirmation token for a new subscriber.".into(),
)
})?;
transaction.commit().await.map_err(|e| {
SubscribeError::UnexpectedError(
Box::new(e),
"Failed to commit SQL transaction to store a new subscriber.".into(),
)
})?;
send_confirmation_email(/* */)
.await
.map_err(|e| {
SubscribeError::UnexpectedError(
Box::new(e),
"Failed to send a confirmation email.".into()
)
})?;
// [..]
}
It is somewhat ugly, but it works:
INFO: [HTTP REQUEST - END]
exception.details=
"Failed to insert new subscriber in the database.
Caused by:
error returned from database: column 'email' of
relation 'subscriptions' does not exist"
exception.message="Failed to insert new subscriber in the database."
Using anyhow
As Opaque Error Type
We could spend more time polishing the machinery we just built, but it turns out it is not necessary: we can lean on the ecosystem, again.
The author of thiserror
9 has another crate for us — anyhow
.
#! Cargo.toml
[dependencies]
# [...]
anyhow = "1"
The type we are looking for is anyhow::Error
. Quoting the documentation:
anyhow::Error
is a wrapper around a dynamic error type.
anyhow::Error
works a lot likeBox<dyn std::error::Error>
, but with these differences:
anyhow::Error
requires that the error isSend
,Sync
, and'static
.anyhow::Error
guarantees that a backtrace is available, even if the underlying error type does not provide one.anyhow::Error
is represented as a narrow pointer — exactly one word in size instead of two.
The additional constraints (Send
, Sync
and 'static
) are not an issue for us.
We appreciate the more compact representation and the option to access a backtrace, if we were to be interested in it.
Let’s replace Box<dyn std::error::Error>
with anyhow::Error
in SubscribeError
:
//! src/routes/subscriptions.rs
// [...]
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
}
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
// [...]
// Back to a single field
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
We got rid of the second String
field as well in SubscribeError::UnexpectedError
— it is no longer necessary.
anyhow::Error
provides the capability to enrich an error with additional context out of the box.
//! src/routes/subscriptions.rs
use anyhow::Context;
// [...]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
let mut transaction = pool
.begin()
.await
.context("Failed to acquire a Postgres connection from the pool")?;
let subscriber_id = insert_subscriber(/* */)
.await
.context("Failed to insert new subscriber in the database.")?;
// [..]
store_token(/* */)
.await
.context("Failed to store the confirmation token for a new subscriber.")?;
transaction
.commit()
.await
.context("Failed to commit SQL transaction to store a new subscriber.")?;
send_confirmation_email(/* */)
.await
.context("Failed to send a confirmation email.")?;
// [...]
}
The context
method is performing double duties here:
- it converts the error returned by our methods into an
anyhow::Error
; - it enriches it with additional context around the intentions of the caller.
context
is provided by the Context
trait — anyhow
implements it for Result
10, giving us access to a fluent API to easily work with fallible functions of all kinds.
anyhow
Or thiserror
?
We have covered a lot of ground — time to address a common Rust myth:
anyhow
is for applications,thiserror
is for libraries.
It is not the right framing to discuss error handling.
You need to reason about intent.
Do you expect the caller to behave differently based on the failure mode they encountered?
Use an error enumeration, empower them to match on the different variants. Bring in thiserror
to write less boilerplate.
Do you expect the caller to just give up when a failure occurs? Is their main concern reporting the error to an operator or a user?
Use an opaque error, do not give the caller programmatic access to the error inner details. Use anyhow
or eyre
if you find their API convenient.
The misunderstanding arises from the observation that most Rust libraries return an error enum instead of Box<dyn std::error::Error>
(e.g. sqlx::Error
).
Library authors cannot (or do not want to) make assumptions on the intent of their users. They steer away from being opinionated (to an extent) — enums give users more control, if they need it.
Freedom comes at a price — the interface is more complex, users need to sift through 10+ variants trying to figure out which (if any) deserve special handling.
Reason carefully about your usecase and the assumptions you can afford to make in order to design the most appropriate error type — sometimes Box<dyn std::error::Error>
or anyhow::Error
are the most appropriate choice, even for libraries.
Who Should Log Errors?
Let’s look again at the logs emitted when a request fails.
# sqlx logs are a bit spammy, cutting them out to reduce noise
export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
There are three error-level log records:
- one emitted by our code in
insert_subscriber
//! src/routes/subscriptions.rs
// [...]
pub async fn insert_subscriber(/* */) -> Result<Uuid, sqlx::Error> {
// [...]
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
// [...]
}
- one emitted by
actix_web
when convertingSubscribeError
into anactix_web::Error
; - one emitted by
tracing_actix_web::TracingLogger
, our telemetry middleware.
We do not need to see the same information three times — we are emitting unnecessary log records which, instead of helping, make it more confusing for operators to understand what is happening (are those logs reporting the same error? Am I dealing with three different errors?).
As a rule of thumb,
errors should be logged when they are handled.
If your function is propagating the error upstream (e.g. using the ?
operator), it should not log the error. It can, if it makes sense, add more context to it.
If the error is propagated all the way up to the request handler, delegate logging to a dedicated middleware — tracing_actix_web::TracingLogger
in our case.
The log record emitted by actix_web
is going to be removed in the next release. Let’s ignore it for now.
Let’s review the tracing::error
statements in our own code:
//! src/routes/subscriptions.rs
// [...]
pub async fn insert_subscriber(/* */) -> Result<Uuid, sqlx::Error> {
// [...]
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
// This needs to go, we are propagating the error via `?`
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
// [..]
}
pub async fn store_token(/* */) -> Result<(), StoreTokenError> {
sqlx::query!(/* */)
.execute(transaction)
.await
.map_err(|e| {
// This needs to go, we are propagating the error via `?`
tracing::error!("Failed to execute query: {:?}", e);
StoreTokenError(e)
})?;
Ok(())
}
Check the logs again to confirm they look pristine.
Summary
We used this chapter to learn error handling patterns «the hard way» — building an ugly but working prototype first, refining it later using popular crates from the ecosystem.
You should now have:
- a solid grasp on the different purposes fulfilled by errors in an application;
- the most appropriate tools to fulfill them.
Internalise the mental model we discussed (Location
as columns, Purpose
as rows):
Internal | At the edge | |
---|---|---|
Control Flow | Types, methods, fields | Status codes |
Reporting | Logs/traces | Response body |
Practice what you learned: we worked on the subscribe
request handler, tackle confirm
as an exercise to verify your understanding of the concepts we covered. Improve the response returned to the user when validation of form data fails.
You can look at the code in the GitHub repository as a reference implementation.
Some of the themes we discussed in this chapter (e.g. layering and abstraction boundaries) will make another appearance when talking about the overall layout and structure of our application. Something to look forward to!
This article is a sample from Zero To Production In Rust, a hands-on introduction to backend development in Rust.
You can get a copy of the book at zero2prod.com.
Click to expand!
Book — Table Of Contents
Click to expand!
The Table of Contents is provisional and might change over time. The draft below is the most accurate picture at this point in time.
- Who Is This Book For
- What Is This Book About
- Getting Started
- Installing The Rust Toolchain
- Project Setup
- IDEs
- Continuous Integration
- Our Driving Example
- What Should Our Newsletter Do?
- Working In Iterations
- Sign Up A New Subscriber
- Choosing A Web Framework
- Our First Endpoint: A Basic Health Check
- Our First Integration Test
- Reading Request Data
- Adding A Database
- Persisting A New Subscriber
- Telemetry
- Unknown Unknowns
- Observability
- Logging
- Instrumenting /POST subscriptions
- Structured Logging
- Go Live
- We Must Talk About Deployments
- Choosing Our Tools
- A Dockerfile For Our Application
- Deploy To DigitalOcean Apps Platform
- Rejecting Invalid Subscribers #1
- Requirements
- First Implementation
- Validation Is A Leaky Cauldron
- Type-Driven Development
- Ownership Meets Invariants
- Panics
- Error As Values —
Result
- Reject Invalid Subscribers #2
- Confirmation Emails
EmailClient
, Our Email Delivery Component- Skeletons And Principles For A Maintainable Test Suite
- Zero Downtime Deployments
- Multi-step Database Migrations
- Sending A Confirmation Email
- Database Transactions
- Error Handling
- What Is The Purpose Of Errors?
- Error Reporting For Operators
- Errors For Control Flow
- Avoid «Ball Of Mud» Error Enums
- Who Should Log Errors?
- Naive Newsletter Delivery
- User Stories Are Not Set In Stone
- Do Not Spam Unconfirmed Subscribers
- All Confirmed Subscribers Receive New Issues
- Implementation Strategy
- Body Schema
- Fetch Confirmed Subscribers List
- Send Newsletter Emails
- Validation Of Stored Data
- Limitations Of The Naive Approach
- Securing Our API
- Authentication
- Password-based Authentication
- Is it safe?
- What Should We Do Next
- Fault-tolerant Newsletter Delivery