Раст ошибка 404

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

@cnmade

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'

@durka

This always happens for stable, but the installation should continue anyway.

@nodakai

@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?

@cengiz-io

@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!

@cnmade

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) or None)
  • Result<T, E> for results from operations that might error (e.g. Ok(val) vs Err(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

  1. Read a file
  2. 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:

  1. When we open a file from path, it either can return a filehandle to work with Ok(file), or it causes an error Err(e). With match f we’re forced to deal with the two possible states. Either we assign the filehandle to f (notice the shadowing of f), or we return from the function by returning the error. The return statement here is important as we want to exit the function.
  2. We then want to read the contents into s, the string we just created. It again can either succeed or throw an error. The function f.read_to_string returns the length of bytes read, so we can safely ignore the value and return an Ok(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 the match 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:

  1. In both cases you’re expected to deal with the two possible states. You can’t continue if don’t do something
  2. 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:

  1. In all cases that might cause an error, we’re calling unwrap() to get to the value
  2. We wrap the result in an Ok variant which we return. We could just return s and drop the Result<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.rs

impl<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)
}
  1. "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 from std::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?

  1. We are still explicit, we have to do something! You can still find all the spots where errors can happen!
  2. 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)
}

  1. These two spots can cause io::Error, as we know from the previous examples
  2. 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.

Memory layout of Box and Box

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

  1. We derive the Debug trait.
  2. Our ParseArgumentsError is a tuple struct with one element: A custom message
  3. We implement std::error::Error for ParseArgumentsError. No need to implement anything else
  4. 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.

The Rust Programming Language Forum

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 Boxing 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

  1. 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
  2. Error Reporting For Operators
    • Keeping Track Of The Error Root Cause
    • The Error Trait
      • Trait Objects
      • Error::source
  3. Errors For Control Flow
    • Layering
    • Modelling Errors as Enums
    • The Error Type Is Not Enough
    • Removing The Boilerplate With thiserror
  4. Avoid «Ball Of Mud» Error Enums
    • Using anyhow As Opaque Error Type
    • anyhow Or thiserror?
  5. Who Should Log Errors?
  6. 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 and actix-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 from std::error through actix_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 with into().

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 and Display), 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 enums 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 the Display representation of the enum variant it is applied to. E.g. Display will return Failed to send a confirmation email. when invoked on an instance of SubscribeError::SendEmailError. You can interpolate values in the final representation — e.g. the {0} in #[error("{0}")] on top of ValidationError is referring to the wrapped String 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 in Error::source;

  • #[from] automatically derives an implementation of From 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 thiserror9 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 like Box<dyn std::error::Error>, but with these differences:

  • anyhow::Error requires that the error is Send, 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 Result10, 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 converting SubscribeError into an actix_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
  1. Getting Started
    • Installing The Rust Toolchain
    • Project Setup
    • IDEs
    • Continuous Integration
  2. Our Driving Example
    • What Should Our Newsletter Do?
    • Working In Iterations
  3. 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
  4. Telemetry
    • Unknown Unknowns
    • Observability
    • Logging
    • Instrumenting /POST subscriptions
    • Structured Logging
  5. Go Live
    • We Must Talk About Deployments
    • Choosing Our Tools
    • A Dockerfile For Our Application
    • Deploy To DigitalOcean Apps Platform
  6. Rejecting Invalid Subscribers #1
    • Requirements
    • First Implementation
    • Validation Is A Leaky Cauldron
    • Type-Driven Development
    • Ownership Meets Invariants
    • Panics
    • Error As Values — Result
  7. 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
  8. 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?
  9. 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
  10. Securing Our API
    • Authentication
    • Password-based Authentication
    • Is it safe?
    • What Should We Do Next
  11. Fault-tolerant Newsletter Delivery

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Раст blacklisted device ошибка
  • Раст ошибка 30015
  • Рассчитать среднюю ошибку аппроксимации для однофакторной линейной модели
  • Раст ошибка 30005 create failed with 32
  • Рассчитать ошибку прямого измерения

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии