Rust custom error

Define custom errors without boilerplate using the custom_error! macro. - GitHub - lovasoa/custom_error: Define custom errors without boilerplate using the custom_error! macro.

Rust custom error

Rust
Crates.io
docs.rs

This crate contains a macro that should make it easier
to define custom errors without having to write a lot of boilerplate code.

The custom_error! macro included in this crate takes a type name
and a list of possible errors and generates a rust enumeration for all the cases,
together with the required impl blocks implementing std::error::Error
and std::fmt::Display.

If you only have a single case for an error you can also generate a struct
instead of an enum.

You can now write:

extern crate custom_error;
use custom_error::custom_error;

// Note the use of braces rather than parentheses.
custom_error!{MyError
    Unknown{code:u8} = "unknown error with code {code}.",
    Err41            = "Sit by a lake"
}

instead of

#[derive(Debug)]
enum MyError {
    Unknown { code: u8 },
    Err41,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter)
    -> std::fmt::Result {
        match self {
            MyError::Unknown { code } => write!(f, "unknown error with code {}." , code),
            MyError::Err41 => write!(f, "Sit by a lake")
        }
    }
}

If you only have a single error case you can also generate a struct:

extern crate custom_error;
use custom_error::custom_error;

custom_error!{MyError{code:u8} = "error with code {code}."}

Simple error

To define a simple error, you only have to indicate three things:

  • the name of your custom error type,
  • the name of the different error cases,
  • a human-readable description for each case.
extern crate custom_error;
use custom_error::custom_error;

custom_error!{MyError
    Bad      = "Something bad happened",
    Terrible = "This is a very serious error!!!"
}

Custom error with parameters

You can store data inside your errors.
In order to do so, indicate the name and types of the fields to want to store in curly braces
after an error type.

extern crate custom_error;
use custom_error::custom_error;

custom_error!{SantaError
    BadChild{name:String, foolishness:u8} = "{name} has been bad {foolishness} times this year",
    TooFar                                = "The location you indicated is too far from the north pole",
    InvalidReindeer{legs:u8}              = "The reindeer has {legs} legs"
}

assert_eq!(
    "Thomas has been bad 108 times this year",
    SantaError::BadChild{name: "Thomas".into(), foolishness: 108}.to_string());

The error messages can reference your parameters using braces ({parameter_name}).
If you need some custom logic to display your parameters, you can use
advanced custom error messages.

Wrapping other error types

If the cause of your error is another lower-level error, you can indicate that
by adding a special source field to one of your error cases.

Thus, you can make your custom error wrap other error types,
and the conversion from these foreign error types will be implemented automatically.

#[macro_use] extern crate custom_error;
use std::{io, io::Read, fs::File, result::Result, num::ParseIntError};

custom_error! {FileParseError
    Io{source: io::Error}         = "unable to read from the file",
    Format{source: ParseIntError} = "the file does not contain a valid integer",
    TooLarge{value:u8}            = "the number in the file ({value}) is too large"
}

fn parse_hex_file(filename: &str) -> Result<u8, FileParseError> {
    let mut contents = String::new();
    // The '?' notation can convert from generic errors to our custom error type
    File::open(filename)?.read_to_string(&mut contents)?;
    let value = u8::from_str_radix(&contents, 16)?;
    if value > 42 {
        Err(FileParseError::TooLarge { value })
    } else {
        Ok(value)
    }
}

fn main() {
    let parse_result = parse_hex_file("/i'm not a file/");
    assert_eq!("unable to read from the file", parse_result.unwrap_err().to_string());
}

Visibility

You can make an error type public by adding the pub keyword
at the beginning of the declaration.

custom_error!{pub MyError A="error a", B="error b"}

Attributes

You can derive traits for your error types by adding attributes
to the beginning of your macro invocation.

custom_error!{#[derive(PartialEq,PartialOrd)] MyError A="error a", B="error b"}
assert!(MyError::A < MyError::B);

Since doc comments
are just syntax sugar for #[doc = "..."], you can use them too:

custom_error!{
   /// This is the documentation for my error type
   pub MyError A="error a", B="error b"
}

Advanced custom error messages

If you want to use error messages that you cannot express with
simple formatting strings, you can generate your error messages with
custom code.

In the following example, we use this feature to display a
different error message based on the cause of the underlying IO error.

custom_error!{ pub MyError
    Io{source: Error} = @{
        match source.kind() {
            NotFound => "The file does not exist",
            TimedOut => "The operation timed out",
            _ => "unknown error",
        }
    },
    Unknown = "unknown error"
}

nostd

This crate supports no-std: it can be built without the rust standard library.
To use the no-std version, disable the std feature in this crate in your Cargo.toml :

[dependencies]
custom_error = { version = "1", default-features = false } # nostd compatible

unstable

There is also an unstable feature
that implements the nostd error trait on AllocError and TryReserveError in no-std.

Minimum supported rust version

This crate works and is tested with rust >= 1.36

Cover image for How to Handle Errors in Rust: A Comprehensive Guide

Nathan

Nathan

Posted on Dec 2, 2022

• Updated on Dec 15, 2022

Rust community constantly discusses about error handling.. In this article I will try to explain what is it then why, and how we should use it.

Purpose of Error Handling

Error handling is a process that helps to identify, debug, and resolve errors that occur during the execution of a program.
It helps to ensure the smooth functioning of the program by preventing errors from occurring and allows the program to continue running in an optimal state.
Error handling also allows users to be informed of any problems that may arise and take corrective action to prevent the errors from happening again in the future.

What is a Result?

Result is a built-in enum in the Rust standard library.
It has two variants Ok(T) and Err(E).

Image description

Result should be used as a return type for a function that can encounter error situations.
Ok value is return in case of success or an Err value in case of an error.

Implementation of Result in a function.

Image description

What is Error Handling

Sometimes we are using functions that can fail, for example calling an endpoint from an API or searching a file. These type of function can encounter errors (in our case the API is not reachable or the file is not existing).
There are similar scenarios where we are using Error Handling.

Image description

Explained Step by Step

  • A Result is the result of the read username from file function.
    It follows that the function’s returned value will either be an Ok that contains a String or an Err that contains an instance of io::Error.

There is a call to «File::open» inside of read username from file, which returns a Result type.

  • It can return an Ok
  • It can return an Err

Then the code calls a match to check the result of the function and return the value inside the ok in the case the function was successful or return the Error value.

In the second function read_to_string, the same principle is applied, but in this case we did not use the keyword return as you can see, and we finally return either an OK or an Err.

So you may ask: On every result type I have to write all these Match block?

So hopefully there is a shortcut :)

Image description

What is the Question Mark- Propagation Error?

According to the rust lang book:

The question mark operator (?) unwraps valid values or returns erroneous values, propagating them to the calling function. It is a unary postfix operator that can only be applied to the types Result and Option.

Let’s me explain it.

Question mark (?) in Rust is used to indicate a Result type. It is used to return an error value if the operation cannot be completed.
For example, in our function that reads a file, it can return a Result type, where the question mark indicates that an error might be returned if the file cannot be read, or in the other hand the final result.
In other words, used to short-circuit a chain of computations and return early if a condition is not met.

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

Enter fullscreen mode

Exit fullscreen mode

Every time you see a ?, that’s a possible early return from the function in case of Error, else , f will hold the file handle the Ok contained and execution of the function continues (similary to unwrap function).

Why use crates for Handle errors?

Standard library does not provide all solutions for Error Handling..
In fact, different errors may be returned by the same function, making it increasingly difficult to handle them precisely.
Personal anecdote, in our company we developed Cherrybomb an API security tool written in Rust, and we need to re-write a good part of it to have a better errors handling.

For example:

Image description

Or the same message error can be displayed multiples times.

Image description

This is why we need to define our own custom Error enum.

Image description

Then our function will look like:

Image description

Customize Errors

Thiserror focuses on creating structured errorsand has only one trait that can be used to define new errors:

Thiserror is an error-handling library for Rust that provides a powerful yet concise syntax to create custom error types.

In the cargo toml:

[dependencies]
thiserror = "1.0"

It allows developers to create custom error types and handlers without having to write a lot of boilerplate code.

Thank to thiserror crate, we can customize our error messages.

Image description

It also provides features to automatically convert between custom error types and the standard error type. We will see it in the next Chapter with Dynamic Error.

  • Create new errors through #[derive(Error)].
  • Enums, structs with named fields, tuple structs, and unit structs are all possible.
  • A Display impl is generated for your error if you provide #[error(«…»)] messages on the struct or each variant of your enum and support string interpolation.

Example taken from docs.rs:

Image description

Dealing Dynamic Errors handling

If you want to be able to use?, your Error type must implement the From trait for the error types of your dependencies. Your program or library may use many dependencies, each of which has its own error you have two different structs of custom error, and we call a function that return one specific type.
For example:
Image description

So when we call our main function that return a ErrorA type, we encounter the following error:

Image description

So one of the solution is to implement the trait From<ErrorB> for the struct ErrorA.

Our code looks like this now:

Image description

Another solution to this problem is to return dynamic errors.
To handle dynamic errors in Rust, in the case of an Err value, you can use the box operator to return the error as a Box (a trait object of the Error trait). This allows the error type to be determined at runtime, rather than at compile time, making it easier to work with errors of different types.

The Box can then be used to store any type of Error, including those from external libraries or custom errors. The Box can then be used to propagate the Error up the call stack, allowing for appropriate handling of the error at each stage.

Image description

Thiserror crate

In order to have a code clearer and soft let’s use thiserror crate.
The thiserror crate can help handle dynamic errors in Rust by allowing the user to define custom error types. It does this through the #[derive(thiserror::Error)] macro. This macro allows the user to define a custom error type with a specific set of parameters, such as an error code, a message, and the source of the error. The user can then use this error type to return an appropriate error value in the event of a dynamic error. Additionally, the thiserror crate also provides several helpful methods, such as display_chain, which can be used to chain together multiple errors into a single error chain.
In the following we have created our error type ErrorB , then we used the From trait to convert from ErrorB errors into our custom ErrorA error type. If a dynamic error occurs, you can create a new instance of your error type and return it to the caller. See function returns_error_a() in line 13.

Image description

Anyhow crate

anyhow was written by the same author, dtolnay, and released in the same week as thiserror.
The anyhow can be used to return errors of any type that implement the std::error::Error trait and will display a nicely formatted error message if the program crashes.
The most common way to use the crate is to wrap your code in a Result type. This type is an alias for the std::result::Result<T, E> type, and it allows you to handle success or failure cases separately.

Image description

When an error occurs,for example you can use the context() method to provide more information about the error, or use the with_chain() method to chain multiple errors together.
The anyhow crate provides several convenient macros to simplify the process of constructing and handling errors. These macros include the bail!() and try_with_context!()macros.
The former can be used to quickly construct an error value, while the latter can be used to wrap a function call and automatically handle any errors that occur.

Comparison

The main difference between anyhow and the Thiserror crate in Rust is the way in which errors are handled. Anyhow allows for error handling using any type that implements the Error trait, whereas Thiserror requires you to explicitly define the error types using macros.

Anyhow is an error-handling library for Rust that provides an easy way to convert errors into a uniform type. It allows to write concise and powerful error-handling code by automatically converting many different types of errors into a single, common type.

In conclusion,in Cherrybomb we choose to combining the two, in order to create a custom error type with thiserror and managed it by the anyhow crate.

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

Defining your own error type

When starting a new project, defining your own error type up-front is a good thing to do.
It will result in more ergonomic code, as you can return the same error type from most
functions and can get it to work with the ? operator. This is especially important if
you use external crates and want to ‘amalgamate’ their error types.

The strategy defined here is the TL;DR summary of a blog post by Rustmeister BurntSushi.
It doesn’t use any external ‘error helper’ crates.

Step 1 — define a custom error type

This type is a union of all possible error types your program needs to handle:

#[derive(Debug)]
pub enum MyErrorType {
    // Errors from external libraries...
    Io(io::Error),
    Git(git2::Error),
    // Errors raised by us...
    Regular(ErrorKind),
    Custom(String)
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ErrorKind {
    NotFound,
    NotAuthorized,
    // etc

impl ErrorKind {
    fn as_str(&self) -> &str {
        match *self {
            ErrorKind::NotFound => "not found",
            ErrorKind::NotAuthorized => "not authorized"
        }
    }
}

This ‘kind enum’ design is used by the Rust standard IO library.
The as_str will help in the next step.

Step 2 — Implement ‘Error’ and ‘Display’ traits

Unfortunately Error is limited to returning a static string constant:

impl Error for MyErrorType {
    fn description(&self) -> &str {
        match *self {
            MyErrorType::Io(ref err) => err.description(),
            MyErrorType::Git(ref err) => err.description(),
            MyErrorType::Regular(ref err) => err.as_str(),
            MyErrorType::Custom(ref err) => err,
        }
    }
}

fmt::Display is more flexible:

impl fmt::Display for MyErrorType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            MyErrorType::Io(ref err) => err.fmt(f),
            MyErrorType::Git(ref err) => err.fmt(f),
            MyErrorType::Regular(ref err) => write!(f, "A regular error occurred {:?}", err),
            MyErrorType::Custom(ref err) => write!(f, "A custom error occurred {:?}", err),
        }
    }
}

Step 3 — Implement ‘From’ for the external error types

This enables the ? operator:

impl From<io::Error> for MyErrorType {
    fn from(err: io::Error) -> MyErrorType {
        MyErrorType::Io(err)
    }
}

impl From<git2::Error> for MyErrorType {
    fn from(err: git2::Error) -> MyErrorType {
        MyErrorType::Git(err)
    }
}

Step 4 — Optional — Create a ‘Result<T, E>’ alias

pub type Result<T> = std::result::Result<T, MyErrorType>;

This does nothing other than simplify (some would say make more opaque)
your function signatures.

Step 5 — Use it!

fn some_func() -> Result<usize> {
    // Possible this will generate a std::io::Error.
    let _f = std::fs::File::create("aa")?;

    // Possible this will generate a git2::Error.
    let _g = Repository::init("/path/to/a/repo")?;

    // Return one of my one errors.
    Err(MyErrorType::Regular(ErrorKind::NotAuthorized))
}

Rust custom error

This crate contains a macro that should make it easier
to define custom errors without having to write a lot of boilerplate code.

The custom_error! macro included in this crate takes a type name
and a list of possible errors and generates a rust enumeration for all the cases,
together with the required impl blocks implementing std::error::Error
and std::fmt::Display.

If you only have a single case for an error you can also generate a struct
instead of an enum.

You can now write:

extern crate custom_error;
use custom_error::custom_error;

// Note the use of braces rather than parentheses.
custom_error!{MyError
    Unknown{code:u8} = "unknown error with code {code}.",
    Err41            = "Sit by a lake"
}

instead of

#[derive(Debug)]
enum MyError {
    Unknown { code: u8 },
    Err41,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter)
    -> std::fmt::Result {
        match self {
            MyError::Unknown { code } => write!(f, "unknown error with code {}." , code),
            MyError::Err41 => write!(f, "Sit by a lake")
        }
    }
}

If you only have a single error case you can also generate a struct:

extern crate custom_error;
use custom_error::custom_error;

custom_error!{MyError{code:u8} = "error with code {code}."}

Simple error

To define a simple error, you only have to indicate three things:

  • the name of your custom error type,
  • the name of the different error cases,
  • a human-readable description for each case.
extern crate custom_error;
use custom_error::custom_error;

custom_error!{MyError
    Bad      = "Something bad happened",
    Terrible = "This is a very serious error!!!"
}

Custom error with parameters

You can store data inside your errors.
In order to do so, indicate the name and types of the fields to want to store in curly braces
after an error type.

extern crate custom_error;
use custom_error::custom_error;

custom_error!{SantaError
    BadChild{name:String, foolishness:u8} = "{name} has been bad {foolishness} times this year",
    TooFar                                = "The location you indicated is too far from the north pole",
    InvalidReindeer{legs:u8}              = "The reindeer has {legs} legs"
}

assert_eq!(
    "Thomas has been bad 108 times this year",
    SantaError::BadChild{name: "Thomas".into(), foolishness: 108}.to_string());

The error messages can reference your parameters using braces ({parameter_name}).
If you need some custom logic to display your parameters, you can use
advanced custom error messages.

Wrapping other error types

If the cause of your error is another lower-level error, you can indicate that
by adding a special source field to one of your error cases.

Thus, you can make your custom error wrap other error types,
and the conversion from these foreign error types will be implemented automatically.

#[macro_use] extern crate custom_error;
use std::{io, io::Read, fs::File, result::Result, num::ParseIntError};

custom_error! {FileParseError
    Io{source: io::Error}         = "unable to read from the file",
    Format{source: ParseIntError} = "the file does not contain a valid integer",
    TooLarge{value:u8}            = "the number in the file ({value}) is too large"
}

fn parse_hex_file(filename: &str) -> Result<u8, FileParseError> {
    let mut contents = String::new();
     The '?' notation can convert from generic errors to our custom error type
    File::open(filename)?.read_to_string(&mut contents)?;
    let value = u8::from_str_radix(&contents, 16)?;
    if value > 42 {
        Err(FileParseError::TooLarge { value })
    } else {
        Ok(value)
    }
}

fn main() {
    let parse_result = parse_hex_file("/i'm not a file/");
    assert_eq!("unable to read from the file", parse_result.unwrap_err().to_string());
}

Visibility

You can make an error type public by adding the pub keyword
at the beginning of the declaration.

custom_error!{pub MyError A="error a", B="error b"}

Attributes

You can derive traits for your error types by adding attributes
to the beginning of your macro invocation.

custom_error!{#[derive(PartialEq,PartialOrd)] MyError A="error a", B="error b"}
assert!(MyError::A < MyError::B);

Since doc comments
are just syntax sugar for #[doc = "..."], you can use them too:

custom_error!{
   /// This is the documentation for my error type
   pub MyError A="error a", B="error b"
}

Advanced custom error messages

If you want to use error messages that you cannot express with
simple formatting strings, you can generate your error messages with
custom code.

In the following example, we use this feature to display a
different error message based on the cause of the underlying IO error.

custom_error!{ pub MyError
    Io{source: Error} = @{
        match source.kind() {
            NotFound => "The file does not exist",
            TimedOut => "The operation timed out",
            _ => "unknown error",
        }
    },
    Unknown = "unknown error"
}

Понравилась статья? Поделить с друзьями:
  • Rust compile error macro
  • Rust cheat checker ошибка вас не вызвали на проверку
  • Rust application error 1000
  • Russian error page
  • Rush royale код ошибки 13