pub struct Error { /* private fields */ }
Expand description
The Error
type, a wrapper around a dynamic error type.
Error
works a lot like Box<dyn std::error::Error>
, but with these
differences:
Error
requires that the error isSend
,Sync
, and'static
.Error
guarantees that a backtrace is available, even if the underlying
error type does not provide one.Error
is represented as a narrow pointer — exactly one word in
size instead of two.
Display representations
When you print an error object using “{}” or to_string(), only the outermost
underlying error or context is printed, not any of the lower level causes.
This is exactly as if you had called the Display impl of the error from
which you constructed your anyhow::Error.
Failed to read instrs from ./path/to/instrs.json
To print causes as well using anyhow’s default formatting of causes, use the
alternate selector “{:#}”.
Failed to read instrs from ./path/to/instrs.json: No such file or directory (os error 2)
The Debug format “{:?}” includes your backtrace if one was captured. Note
that this is the representation you get by default if you return an error
from fn main
instead of printing it explicitly yourself.
Error: Failed to read instrs from ./path/to/instrs.json
Caused by:
No such file or directory (os error 2)
and if there is a backtrace available:
Error: Failed to read instrs from ./path/to/instrs.json
Caused by:
No such file or directory (os error 2)
Stack backtrace:
0: <E as anyhow::context::ext::StdError>::ext_context
at /git/anyhow/src/backtrace.rs:26
1: core::result::Result<T,E>::map_err
at /git/rustc/src/libcore/result.rs:596
2: anyhow::context::<impl anyhow::Context<T,E> for core::result::Result<T,E>>::with_context
at /git/anyhow/src/context.rs:58
3: testing::main
at src/main.rs:5
4: std::rt::lang_start
at /git/rustc/src/libstd/rt.rs:61
5: main
6: __libc_start_main
7: _start
To see a conventional struct-style Debug representation, use “{:#?}”.
Error {
context: "Failed to read instrs from ./path/to/instrs.json",
source: Os {
code: 2,
kind: NotFound,
message: "No such file or directory",
},
}
If none of the built-in representations are appropriate and you would prefer
to render the error and its cause chain yourself, it can be done something
like this:
use anyhow::{Context, Result};
fn main() {
if let Err(err) = try_main() {
eprintln!("ERROR: {}", err);
err.chain().skip(1).for_each(|cause| eprintln!("because: {}", cause));
std::process::exit(1);
}
}
fn try_main() -> Result<()> {
...
}
Available on crate feature std
only.
Create a new error object from any error type.
The error type must be threadsafe and 'static
, so that the Error
will be as well.
If the error type does not provide a backtrace, a backtrace will be
created here to ensure that a backtrace exists.
Create a new error object from a printable error message.
If the argument implements std::error::Error, prefer Error::new
instead which preserves the underlying error’s cause chain and
backtrace. If the argument may or may not implement std::error::Error
now or in the future, use anyhow!(err)
which handles either way
correctly.
Error::msg("...")
is equivalent to anyhow!("...")
but occasionally
convenient in places where a function is preferable over a macro, such
as iterator or stream combinators:
use anyhow::{Error, Result};
use futures::stream::{Stream, StreamExt, TryStreamExt};
async fn demo<S>(stream: S) -> Result<Vec<Output>>
where
S: Stream<Item = Input>,
{
stream
.then(ffi::do_some_work) // returns Result<Output, &str>
.map_err(Error::msg)
.try_collect()
.await
}
Wrap the error value with additional context.
For attaching context to a Result
as it is propagated, the
Context
extension trait may be more convenient than
this function.
The primary reason to use error.context(...)
instead of
result.context(...)
via the Context
trait would be if the context
needs to depend on some data held by the underlying error:
use anyhow::Result;
use std::fs::File;
use std::path::Path;
struct ParseError {
line: usize,
column: usize,
}
fn parse_impl(file: File) -> Result<T, ParseError> {
...
}
pub fn parse(path: impl AsRef<Path>) -> Result<T> {
let file = File::open(&path)?;
parse_impl(file).map_err(|error| {
let context = format!(
"only the first {} lines of {} are valid",
error.line, path.as_ref().display(),
);
anyhow::Error::new(error).context(context)
})
}
Available on nightly
or crate feature backtrace
only.
Get the backtrace for this Error.
In order for the backtrace to be meaningful, one of the two environment
variables RUST_LIB_BACKTRACE=1
or RUST_BACKTRACE=1
must be defined
and RUST_LIB_BACKTRACE
must not be 0
. Backtraces are somewhat
expensive to capture in Rust, so we don’t necessarily want to be
capturing them all over the place all the time.
- If you want panics and errors to both have backtraces, set
RUST_BACKTRACE=1
; - If you want only errors to have backtraces, set
RUST_LIB_BACKTRACE=1
; - If you want only panics to have backtraces, set
RUST_BACKTRACE=1
and
RUST_LIB_BACKTRACE=0
.
Stability
Standard library backtraces are only available on the nightly channel.
Tracking issue: rust-lang/rust#53487.
On stable compilers, this function is only available if the crate’s
“backtrace” feature is enabled, and will use the backtrace
crate as
the underlying backtrace implementation.
[dependencies]
anyhow = { version = "1.0", features = ["backtrace"] }
Available on crate feature std
only.
An iterator of the chain of source errors contained by this Error.
This iterator will visit every error in the cause chain of this error
object, beginning with the error that this error object was created
from.
Example
use anyhow::Error;
use std::io;
pub fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
for cause in error.chain() {
if let Some(io_error) = cause.downcast_ref::<io::Error>() {
return Some(io_error.kind());
}
}
None
}
Available on crate feature std
only.
The lowest level cause of this error — this error’s cause’s
cause’s cause etc.
The root cause is the last error in the iterator produced by
chain()
.
Returns true if E
is the type held by this error object.
For errors with context, this method returns true if E
matches the
type of the context C
or the type of the error on which the
context has been attached. For details about the interaction between
context and downcasting, see here.
Attempt to downcast the error object to a concrete type.
Downcast this error object by reference.
Example
// If the error was caused by redaction, then return a tombstone instead
// of the content.
match root_cause.downcast_ref::<DataStoreError>() {
Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)),
None => Err(error),
}
Downcast this error object by mutable reference.
Converts this type into a shared reference of the (usually inferred) input type.
Converts this type into a shared reference of the (usually inferred) input type.
Available on crate feature std
only.
The resulting type after dereferencing.
Dereferences the value.
Available on crate feature std
only.
Mutably dereferences the value.
Available on crate feature std
only.
Converts to this type from the input type.
Converts to this type from the input type.
Converts to this type from the input type.
Converts to this type from the input type.
🔬This is a nightly-only experimental API. (provide_any
)
Data providers should implement this method to provide all values they are able to
provide by using demand
. Read more
Anyhow ¯_(°ペ)_/¯
This library provides anyhow::Error
, a trait object based error type
for easy idiomatic error handling in Rust applications.
[dependencies] anyhow = "1.0"
Compiler support: requires rustc 1.39+
Details
-
Use
Result<T, anyhow::Error>
, or equivalentlyanyhow::Result<T>
, as the
return type of any fallible function.Within the function, use
?
to easily propagate any error that implements the
std::error::Error
trait.use anyhow::Result; fn get_cluster_info() -> Result<ClusterMap> { let config = std::fs::read_to_string("cluster.json")?; let map: ClusterMap = serde_json::from_str(&config)?; Ok(map) }
-
Attach context to help the person troubleshooting the error understand where
things went wrong. A low-level error like «No such file or directory» can be
annoying to debug without more context about what higher level step the
application was in the middle of.use anyhow::{Context, Result}; fn main() -> Result<()> { ... it.detach().context("Failed to detach the important thing")?; let content = std::fs::read(path) .with_context(|| format!("Failed to read instrs from {}", path))?; ... }
Error: Failed to read instrs from ./path/to/instrs.json Caused by: No such file or directory (os error 2)
-
Downcasting is supported and can be by value, by shared reference, or by
mutable reference as needed.// If the error was caused by redaction, then return a // tombstone instead of the content. match root_cause.downcast_ref::<DataStoreError>() { Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)), None => Err(error), }
-
If using the nightly channel, or stable with
features = ["backtrace"]
, a
backtrace is captured and printed with the error if the underlying error type
does not already provide its own. In order to see backtraces, they must be
enabled through the environment variables described instd::backtrace
:- If you want panics and errors to both have backtraces, set
RUST_BACKTRACE=1
; - If you want only errors to have backtraces, set
RUST_LIB_BACKTRACE=1
; - If you want only panics to have backtraces, set
RUST_BACKTRACE=1
and
RUST_LIB_BACKTRACE=0
.
The tracking issue for this feature is rust-lang/rust#53487.
- If you want panics and errors to both have backtraces, set
-
Anyhow works with any error type that has an impl of
std::error::Error
,
including ones defined in your crate. We do not bundle aderive(Error)
macro
but you can write the impls yourself or use a standalone macro like
thiserror.use thiserror::Error; #[derive(Error, Debug)] pub enum FormatError { #[error("Invalid header (expected {expected:?}, got {found:?})")] InvalidHeader { expected: String, found: String, }, #[error("Missing attribute: {0}")] MissingAttribute(String), }
-
One-off error messages can be constructed using the
anyhow!
macro, which
supports string interpolation and produces ananyhow::Error
.return Err(anyhow!("Missing attribute: {}", missing));
A
bail!
macro is provided as a shorthand for the same early return.bail!("Missing attribute: {}", missing);
No-std support
In no_std mode, the same API is almost all available and works the same way. To
depend on Anyhow in no_std mode, disable our default enabled «std» feature in
Cargo.toml. A global allocator is required.
[dependencies] anyhow = { version = "1.0", default-features = false }
Since the ?
-based error conversions would normally rely on the
std::error::Error
trait which is only available through std, no_std mode will
require an explicit .map_err(Error::msg)
when working with a non-Anyhow error
type inside a function that returns Anyhow’s error type.
Comparison to failure
The anyhow::Error
type works something like failure::Error
, but unlike
failure ours is built around the standard library’s std::error::Error
trait
rather than a separate trait failure::Fail
. The standard library has adopted
the necessary improvements for this to be possible as part of RFC 2504.
Comparison to thiserror
Use Anyhow if you don’t care what error type your functions return, you just
want it to be easy. This is common in application code. Use thiserror if you
are a library that wants to design your own dedicated error type(s) so that on
failures the caller gets exactly the information that you choose.
License
Licensed under either of Apache License, Version
2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
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).
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.
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.
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
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:
Or the same message error can be displayed multiples times.
This is why we need to define our own custom Error enum.
Then our function will look like:
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.
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:
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:
So when we call our main function that return a ErrorA
type, we encounter the following error:
So one of the solution is to implement the trait From<ErrorB>
for the struct ErrorA
.
Our code looks like this now:
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.
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.
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.
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.
Introduction
In this article, I will discuss error handling in Rust
🦀. I try to explain the differences between recoverable and unrecoverable errors, and how to handle them properly in your code.
At the end of this article, I will also take a quick lookinto two popular crates for error handling in Rust
🦀: anyhow
and thiserror
.
The Panic Macro and Unrecoverable Errors
A Panic
is an exception that a Rust
🦀 program can throw. It stops all execution in the current thread. Panic, will return a short description of the error and the location of the panic in the source code.
Let’s look at an example:
fn main() {
println!("Hello, world!");
panic!("oh no!");
}
This will print Hello, world!
and then panic with the message oh no!
and the location of the panic in the source code.
If your running this code in a terminal, you will see the following output:
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The message gives us also a hint on how to display a backtrace. If you run the code with the environment variable RUST_BACKTRACE=1
you will get a list of all the functions leading up to the panic.
RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: rust_error::main
at ./src/main.rs:3:5
3: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
In this case, the backtrace is not very useful, because the panic is in the main
function.
Let’s look at a different example, which is extremely contrived, but for demonstration purposes, it will do.
fn a() {
b();
}
fn b() {
c("engin");
}
fn c(name: &str) {
if name == "engin" {
panic!("Dont pass engin");
}
}
fn main() {
a();
}
We have three functions a
, b
and c
. The main function calls a
. a
calls b
and b
calls c
. c
takes a string as an argument and panics if the string is engin
.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This error is not very useful. We can see that the panic happened in c
, but we don’t know which function called c
.
If we run the code with the environment variable RUST_BACKTRACE=1
we get the following output:
RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: rust_error::c
at ./src/main.rs:11:9
3: rust_error::b
at ./src/main.rs:6:5
4: rust_error::a
at ./src/main.rs:2:5
5: rust_error::main
at ./src/main.rs:16:5
6: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
This is much better. We can see that the panic happened in c
, and we can see the call stack leading up to the panic. We see that c
was called by b
, which was called by a
, which was called by main
. So let’s change the code in b
to call c
with a different name.
fn b() {
c("dirien");
}
Now the code compiles and runs without any problems.
Recoverable Errors
A recoverable error is an error that can be handled by the code. For example, if we try to open a file that does not exist, we can handle the error and print a message to the user or create the file instead of crashing the program.
For this case, we can use the Result
type. The Result
type is an enum with two variants: Ok
and Err
. The Ok
variant indicates that the operation was successful and stores a generic value. The Err
variant indicates that the operation failed and stores an error value.
Like the Option
type, the Result
type is defined in the standard library, and we need to bring it into scope.
Let’s look at an example. We will try to open a file and read the contents of the file.
fn main() {
let f = File::open("hello.txt");
}
Here we need to check the result of the open
function. If the file is opened successfully, we can read the contents of the file. If the file is not opened successfully, we can print an error message to the user.
To check the result of the open
function, we can use the match
expression. The match
expression is similar to the if
expression, but it can handle more than two cases. We’re also shadowing the f
variable and setting it to the match
expression.
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => panic!("There was a problem opening the file: {:?}", error),
};
}
If the open
function returns Ok
, we store the file handle in the f
variable. If the open
function returns Err
, we panic and print the error message.
Let us run the code and see what happens.
cargo run
warning: `rust-error` (bin "rust-error") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/rust-error`
thread 'main' panicked at 'There was a problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:7:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
We get a panic
, but the error message is much more useful. We can see that the error is Os { code: 2, kind: NotFound, message: "No such file or directory" }
. This error makes sense because we are trying to open a file that does not exist.
Now let’s enhance the code instead of panicking, we will create the file if it does not exist. First, we will bring the ErrorKind
enum into scope.
use std::fs::File;
use std::io::ErrorKind;
...
Then we will use the match
expression to check the error kind. If the error kind is NotFound
, we will create the file. But the creation of the file can also fail, so we will use the match
expression again to check the result of the create
function. If the create
function returns Ok
, we will return the file handle. If the create
function returns Err
, we will panic.
The last part is to use other_error
to handle all other errors that are not ErrorKind::NotFound
.
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("There was a problem opening the file: {:?}", other_error),
},
};
}
Now when we run the code, we can see that no panic happens. And if we check the directory, we can see that the file was created.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/rust-error`
But this code is not very readable. We have a lot of match
expressions. A better way to handle this is to use closures. We will use closures to handle the Ok
and Err
variants of the Result
type.
When we attempt to open a file, we will use the unwrap_or_else
method which gives us back the file or calls the anonymous function or closure that we pass the error to. Inside the closure, we will check the error kind. If the error is NotFound
then we attempt to create the file called the unwrap_or_else
method again. This gives us back the file if the calls succeed. Note that we don’t have a semicolon at the end which means this is an expression and not a statement. In the failure case, we have another closure that will just panic.
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("There was a problem opening the file: {:?}", error);
}
});
}
Now we going to rewrite the code again to use the unwrap
and expect
methods. The unwrap method is a shortcut method that is implemented on Result
types. If the Result
is Ok
, the unwrap
method will return the value inside the Ok
. If the Result
is Err
, the unwrap
method will call the panic!
macro for us.
fn main() {
let f = File::open("hello.txt").unwrap();
}
When we run the code, we get the same error as before.
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/rust-error`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The expect
method is similar to the unwrap
method, but we can pass a custom error message to the expect
method. This error message will be printed when the Result
is Err
.
fn main() {
let f = File::open("hello.txt").expect("OMG! I cant open the file!");
}
When we run the code, we can see our custom error message.
cargo run
Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/rust-error`
thread 'main' panicked at 'OMG! I cant open the file!: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
How to propagate errors
In the previous section, we saw how to handle errors. But what if we want to propagate the error to the caller of our function? This gives the caller the ability to handle the error.
Let’s say we want to read the contents of a file. We will create a function that reads username
from a file. The function will return a Result
type. The Result
type will contain a String
on success and the io::Error
on error.
If the file does not exist, we will return the error. If the file exists, we will try to read the contents of the file. If this is not successful, we will return the error. If the read is successful, we will return the username.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
We can shorten the code by using the ?
operator. The ?
operator can only be used in functions that return a Result
type. The ?
operator is similar to our unwrap
and expect
methods. If the Result
is Ok
, the ?
operator will return the value inside the Ok
. If the Result
is Err
, instead of calling the panic!
macro, the ?
operator will return the error and early exit the function.
If everything is successful, the ?
operator will return safely the value inside the Ok
.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
We can shorten the code even more by chaining method calls. The ?
operator can be used with method calls that return a Result
type.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
But we can make the code even shorter by using the system module function fs::read_to_string
. The fs::read_to_string
function will open the file, create a new String
, read the contents of the file into the String
, and return it. If any of these steps fail, the fs::read_to_string
function will return the error.
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
As mentioned before, the ?
operator can only be used in functions that return a Result
type. If we want to use the ?
operator in the main
function, we have to change the return type of the main
function to Result
. The main
function can also return a Result
type.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
The main
function returns a Result
type. The Result
type contains a ()
on success and a Box<dyn Error>
on error.
Error helper crates
There are a lot of crates that can help you with error handling. In this section, we will look at the anyhow
crate and the thiserror
crate. This is not an exhaustive list of error-handling crates, but it will give you an idea of what is out there.
Of course, we can not go to deep into these crates. If you want to learn more about these crates, you can check out the links at the end of this section.
The thiserror
crate
thiserror
provides a derived implementation which adds the error
trait for us. This makes it easier to implement the error
trait for our custom error types.
To use the thiserror
crate, we have to add the crate to our Cargo.toml
file. The cargo add
command will add the thiserror
crate to our Cargo.toml
file.
cargo add thiserror
We can now use the thiserror
crate in our code. We will create a custom error type for our read_username_from_file
function called CustomError
.
use std::error::Error;
use std::fs::File;
use std::io::Read;
#[derive(Debug, thiserror::Error)]
enum CustomError {
#[error("OMG! There is an error {0}")]
BadError(#[from] std::io::Error),
}
fn read_username_from_file() -> Result<String, CustomError> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
The anyhow
crate
anyhow
provides an idiomatic alternative to explicitly handling errors. It is similar to the previously mentioned error
trait but has additional features such as adding context to thrown errors.
To add the anyhow
crate to our project, we can use the cargo add
command.
cargo add anyhow
We can now use the anyhow
crate in our code. We will create a custom error type for our read_username_from_file
function called CustomError
.
use std::fs::File;
use std::io::Read;
use anyhow::Context;
fn read_username_from_file() -> Result<String, anyhow::Error> {
let mut username = String::new();
File::open("hello.txt").context("Failed to open file")?.read_to_string(&mut username).context("Failed to read file")?;
Ok(username)
}
When to use thiserror
and anyhow
The thiserror
crate is useful when you want to implement the Error
trait for your custom error types. The anyhow
crate is useful when you don’t care about the error type and just want to add context to the error.
Summary
In this article, we looked at error handling in Rust
🦀. We talked about non-recoverable errors and recoverable errors. The error handling in Rust
🦀 is designed to help you in writing code that is more robust and less error-prone. The panic!
macro is used for non-recoverable errors when your program is in a state where it can not continue and should stop instead of trying to proceed with invalid or incorrect data. The Result
type is used for recoverable errors. The Result
enums indicate that the operation can fail and that our code can recover from the error and the caller of the piece of code has to handle the success or failure of the operation.
Resources
-
Error Handling
-
The
anyhow
crate -
The
thiserror
crate
Command Line Applications in Rust
Nicer error reporting
We all can do nothing but accept the fact that errors will occur.
And in contrast to many other languages,
it’s very hard not to notice and deal with this reality
when using Rust:
As it doesn’t have exceptions,
all possible error states are often encoded in the return types of functions.
Results
A function like read_to_string
doesn’t return a string.
Instead, it returns a Result
that contains either
a String
or an error of some type
(in this case std::io::Error
).
How do you know which it is?
Since Result
is an enum
,
you can use match
to check which variant it is:
#![allow(unused)] fn main() { let result = std::fs::read_to_string("test.txt"); match result { Ok(content) => { println!("File content: {}", content); } Err(error) => { println!("Oh noes: {}", error); } } }
Unwrapping
Now, we were able to access the content of the file,
but we can’t really do anything with it after thematch
block.
For this, we’ll need to somehow deal with the error case.
The challenge is that all arms of amatch
block need to return something of the same type.
But there’s a neat trick to get around that:#![allow(unused)] fn main() { let result = std::fs::read_to_string("test.txt"); let content = match result { Ok(content) => { content }, Err(error) => { panic!("Can't deal with {}, just exit here", error); } }; println!("file content: {}", content); }
We can use the String in
content
after the match block.
Ifresult
were an error, the String wouldn’t exist.
But since the program would exit before it ever reached a point where we usecontent
,
it’s fine.This may seem drastic,
but it’s very convenient.
If your program needs to read that file and can’t do anything if the file doesn’t exist,
exiting is a valid strategy.
There’s even a shortcut method onResult
s, calledunwrap
:#![allow(unused)] fn main() { let content = std::fs::read_to_string("test.txt").unwrap(); }
No need to panic
Of course, aborting the program is not the only way to deal with errors.
Instead of thepanic!
, we can also easily writereturn
:fn main() -> Result<(), Box<dyn std::error::Error>> { let result = std::fs::read_to_string("test.txt"); let content = match result { Ok(content) => { content }, Err(error) => { return Err(error.into()); } }; Ok(()) }
This, however changes the return type our function needs.
Indeed, there was something hidden in our examples all this time:
The function signature this code lives in.
And in this last example withreturn
,
it becomes important.
Here’s the full example:fn main() -> Result<(), Box<dyn std::error::Error>> { let result = std::fs::read_to_string("test.txt"); let content = match result { Ok(content) => { content }, Err(error) => { return Err(error.into()); } }; println!("file content: {}", content); Ok(()) }
Our return type is a
Result
!
This is why we can writereturn Err(error);
in the second match arm.
See how there is anOk(())
at the bottom?
It’s the default return value of the function and means
“Result is okay, and has no content”.Question Mark
Just like calling
.unwrap()
is a shortcut
for thematch
withpanic!
in the error arm,
we have another shortcut for thematch
thatreturn
s in the error arm:
?
.That’s right, a question mark.
You can append this operator to a value of typeResult
,
and Rust will internally expand this to something very similar to
thematch
we just wrote.Give it a try:
fn main() -> Result<(), Box<dyn std::error::Error>> { let content = std::fs::read_to_string("test.txt")?; println!("file content: {}", content); Ok(()) }
Very concise!
Providing Context
The errors you get when using
?
in yourmain
function are okay,
but they are not great.
For example:
When you runstd::fs::read_to_string("test.txt")?
but the filetest.txt
doesn’t exist,
you get this output:Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
In cases where your code doesn’t literally contain the file name,
it would be very hard to tell which file wasNotFound
.
There are multiple ways to deal with this.For example, we can create our own error type,
and then use that to build a custom error message:#[derive(Debug)] struct CustomError(String); fn main() -> Result<(), CustomError> { let path = "test.txt"; let content = std::fs::read_to_string(path) .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?; println!("file content: {}", content); Ok(()) }
Now,
running this we’ll get our custom error message:Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")
Not very pretty,
but we can easily adapt the debug output for our type later on.This pattern is in fact very common.
It has one problem, though:
We don’t store the original error,
only its string representation.
The often usedanyhow
library has a neat solution for that:
similar to ourCustomError
type,
itsContext
trait can be used to add a description.
Additionally, it also keeps the original error,
so we get a “chain” of error messages pointing out the root cause.Let’s first import the
anyhow
crate by adding
anyhow = "1.0"
to the[dependencies]
section
of ourCargo.toml
file.The full example will then look like this:
use anyhow::{Context, Result}; fn main() -> Result<()> { let path = "test.txt"; let content = std::fs::read_to_string(path) .with_context(|| format!("could not read file `{}`", path))?; println!("file content: {}", content); Ok(()) }
This will print an error:
Error: could not read file `test.txt` Caused by: No such file or directory (os error 2)
Elastio Engineering Handbook
Errors
Rust’s try operator (?
) and support for sum types (e.g., Result
which can be either Ok
or Err
) make for powerful
and easy-to-reason-about error handling. Unfortunately as of this writing in late 2020 there is no consensus solution
for representing and reporting errors in crates. The existing std::result::Result
and std::error::Error
types
provide the bare bones for representing successful and failed operations, but they’re not adequate on their own.
In first year or so of Elastio, we used the snafu crate, which is an opinionated
implementation of error handling in Rust that seemed to work well. Many of our crates still use Snafu today, because
there’s no reason to migrate. However recently momentum has been building behind two crates which together are fast
becoming the de facto Right Way(tm) for doing things: anyhow and
thiserror.
anyhow
provides a universal Error
type which can wrap any std::error::Error
implementation and provide some useful
operations. It’s the equivalent of throws Exception
in Java. We use anyhow
when building CLIs that need to be able
to handle various kinds of errors from different crates and have no need to wrap those errors in a specific error type.
In some cases we also use anyhow
when writing tests, when the alternative would be Box<dyn Error>
which is nasty.
thiserror
is used at Elastio when a library crate needs to expose an error enum as part of it’s API. Where
historically we’ve used snafu
, newer crates (and any crates created in the future) use thiserror
to build a
CrateNameError
variant.
This chapter describes the best practices we’ve evolved with these crates, and should be followed unless there’s a good
reason to do something different.
Official Docs
Most of the details of how to use anyhow
and thiserror
are covered in their respective docs. None of that will be
repeated here, so before reading the rest of this chapter make sure you’ve reviewed the official docs for both crates
and have a good understanding of how they work in general (and, in particular, how anyhow
is different than
thiserror
and under what circumstances one should use one versus the other).
Legacy Snafu
As noted above, many of our biggest and most important crates were written before thiserror
was clearly the way
forward for library error reporting. Those crates use snafu
instead. There’s nothing wrong with snafu
, and it’s
similar in design to thiserror
in many ways. Those crates that use snafu
will continue to do so, and if you find
yourself needing to add or modify the error variants in those crates, you too must use snafu
. Porting existing error
handling to thiserror
without a compelling reason is not a good use of engineering time.
Having said that, starting a new crate in 2020 or beyond and using snafu
to build the error type is also not a good
use of engineering time.
Error variant naming
Each variant of the error enum is obviously an error, because it’s a variant of the error enum. Thus, the word Error
should not appear in the name of the error enum as it’s redundant. So IoError
is a bad name for an error; Io
is
good.
Error messages
thiserror
When defining an error type with thiserror
, it’s easy to define what the error message should be for an error:
#![allow(unused)] fn main() { use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("IO error")] Io { source: std::io::Error } } }
You might be tempted to try to make another error type’s message part of your error, like this:
#![allow(unused)] fn main() { use thiserror::Error; #[derive(Error, Debug)] pub enum Error { // WRONG DON'T DO THIS #[error("IO error: {source}")] Io { source: std::io::Error } } }
You might even find this pattern in our code still. However the latest best practice is to report the error type which
caused a particular error by returning it in thestd::error::Error::source()
function, which forthiserror
means a
field namedsource
or marked with the#[source]
or#[from]
attributes. Why?Because it’s often not at all useful to include the error message of an inner error. Maybe that error is itself just a
higher-level error type, and the real cause of the error is nested three or more errors deep. Or maybe you need to know
the details of all of the errors in the chain from root cause on up to your error type in order to understand what
really happened.Therefore, instead of forcing the nested error’s message into your error message, you should rely on crates like
anyhow
(orcolor-eyre
) to pretty-print error types at the point where they are communicated to users (printed to the
console in the case of a CLI, or written to the log in the case of a server).
anyhow
When reporting errors with
anyhow
, the principle is the same but the mechanics are slightly different. Since
anyhow::Error
is a universal container for any error type, you do not useanyhow
to define strongly typed errors.
Instead you wrap arbitrary error types inanyhow
. But sometimes you have a situation where you have an errore
, and
you want to report it as ananyhow::Error
but with some additional context to help clarify the error. This is the
wrong way:#![allow(unused)] fn main() { use anyhow::anyhow; // DO NOT DO THIS let e = std::io::Error::last_os_error(); // Assume `e` contains an error you want to report anyhow!(format!("Got an error while trying to frobulate the gonkolator: {}", e)); }
This has the same problem as the
thiserror
example above. You’re losing all of the information ine
other than it’s
message. Maybe it had its ownsource
with valuable context, or a backtrace that would have clarified where this
happened. Instead you should useanyhow
to wrap the error in some context:#![allow(unused)] fn main() { use anyhow::anyhow; // This is the right way let e = std::io::Error::last_os_error(); // Assume `e` contains an error you want to report anyhow::Error::new(e).context("Got an error while trying to frobulate the gonkolator"); }
Using anyhow
context
orwith_context
The above example uses
context
on ananyhow::Error
.anyhow
also has aContext
trait which addscontext
and
with_context
to arbitraryResult
types, to make it easier to wrap possible errors in context information.Be advised that in this case you should avoid allocating strings when calling
context
. For example:use anyhow::Context; fn get_username() -> String { // ... "foo".to_owned() } fn get_host() -> String { // ... "foo".to_owned() } fn main() -> anyhow::Result<()> { // WRONG std::fs::File::create("/tmp/f00") .context(format!("Error creating file foo for user {} on host {}", get_username(), get_host()))?; // RIGHT std::fs::File::create("/tmp/f00") .with_context(|| format!("Error creating file foo for user {} on host {}", get_username(), get_host()))?; Ok(()) }
By passing a closure to
with_context
, you defer the evaluation offormat!
unlessFile::create
actually fails. On
success you skip all of this computation and the associated heap allocation, and calls toget_username()
and
get_host()
.
error
moduleEach library crate should have an
error
module. This should define athiserror
-based error enum namedCrateNameError
,
whereCrateName
is the pascal case representation of the crate’s name.Many Rust crates and the Rust std lib use an error representation called
Error
, but this leads to problems with code
clarity when dealing with multiple different crates’ error types.The
error
module should also define a type alias calledResult
, which aliases tostd::result::Result
with the
default error type set to the crate’s error type, e.g.:#![allow(unused)] fn main() { // In `error.rs` pub enum CrateNameError { /* ... */ } pub type Result<T, E = CrateNameError> = std::result::Result<T, E>; }
If necessary (and only if necessary!) the root of each library crate should expose the
error
module publically. As of
now the only reason this would be necessary is to expose the Snafu-generated context selectors to other crates, which is
only needed if macros are generating code that needs to use those error types. That’s an edge case; in generalerror
should be private.In all cases, the error enum and
Result
type alias should be re-exported from the root of the crate, e.g.:// In lib.rs // The `error` module should not be public except for the edge case described above mod error; // but the Error type and the Result type alias should be part of the public API in the root module pub use error::{Result, CrateNameError}; // And all funcs should use this public re-export // Note this is using crate::Result, not crate::error::Result pub fn some_public_func() -> Result<()> { todo!() }
Other modules in the crate should
use crate::CrateNameError
, NOTuse crate::error::CrateNameError
. This is for
consistency between crate code and external callers, and also because it’s less typing.Note that when using Snafu, Snafu’s context selectors should NOT be re-exported this way, and when referenced within other modules in the
crate, those modules shoulduse crate::error
, and refer to the context selectors witherror::SomeErrorKind
.Using
ensure
Anyhow provides the
ensure!
macro to test a condition and fail with an error if it is false. Prefer this to explicit
if
ormatch
expressions unless they add some clarity somehow.
68 stable releases
new 1.0.69 |
Feb 5, 2023 |
---|---|
1.0.68 | Dec 18, 2022 |
1.0.66 | Oct 20, 2022 |
1.0.58 | Jun 18, 2022 |
0.0.0 | Oct 5, 2019 |
#1 in Rust patterns
Download history
969372/week @ 2022-10-21
898252/week @ 2022-10-28
992215/week @ 2022-11-04
955742/week @ 2022-11-11
855829/week @ 2022-11-18
903178/week @ 2022-11-25
910884/week @ 2022-12-02
881247/week @ 2022-12-09
866044/week @ 2022-12-16
533588/week @ 2022-12-23
691677/week @ 2022-12-30
943475/week @ 2023-01-06
913716/week @ 2023-01-13
949346/week @ 2023-01-20
1052273/week @ 2023-01-27
906244/week @ 2023-02-03
3,992,409 downloads per month
Used in 14,288 crates
(7,805 directly)
150KB
2.5K
SLoC
Anyhow ¯_(°ペ)_/¯
This library provides anyhow::Error
, a trait object based error type
for easy idiomatic error handling in Rust applications.
[dependencies]
anyhow = "1.0"
Compiler support: requires rustc 1.39+
Details
-
Use
Result<T, anyhow::Error>
, or equivalentlyanyhow::Result<T>
, as the
return type of any fallible function.Within the function, use
?
to easily propagate any error that implements the
std::error::Error
trait.use anyhow::Result; fn get_cluster_info() -> Result<ClusterMap> { let config = std::fs::read_to_string("cluster.json")?; let map: ClusterMap = serde_json::from_str(&config)?; Ok(map) }
-
Attach context to help the person troubleshooting the error understand where
things went wrong. A low-level error like «No such file or directory» can be
annoying to debug without more context about what higher level step the
application was in the middle of.use anyhow::{Context, Result}; fn main() -> Result<()> { ... it.detach().context("Failed to detach the important thing")?; let content = std::fs::read(path) .with_context(|| format!("Failed to read instrs from {}", path))?; ... }
Error: Failed to read instrs from ./path/to/instrs.json Caused by: No such file or directory (os error 2)
-
Downcasting is supported and can be by value, by shared reference, or by
mutable reference as needed.// If the error was caused by redaction, then return a // tombstone instead of the content. match root_cause.downcast_ref::<DataStoreError>() { Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)), None => Err(error), }
-
If using the nightly channel, or stable with
features = ["backtrace"]
, a
backtrace is captured and printed with the error if the underlying error type
does not already provide its own. In order to see backtraces, they must be
enabled through the environment variables described instd::backtrace
:- If you want panics and errors to both have backtraces, set
RUST_BACKTRACE=1
; - If you want only errors to have backtraces, set
RUST_LIB_BACKTRACE=1
; - If you want only panics to have backtraces, set
RUST_BACKTRACE=1
and
RUST_LIB_BACKTRACE=0
.
The tracking issue for this feature is rust-lang/rust#53487.
- If you want panics and errors to both have backtraces, set
-
Anyhow works with any error type that has an impl of
std::error::Error
,
including ones defined in your crate. We do not bundle aderive(Error)
macro
but you can write the impls yourself or use a standalone macro like
thiserror.use thiserror::Error; #[derive(Error, Debug)] pub enum FormatError { #[error("Invalid header (expected {expected:?}, got {found:?})")] InvalidHeader { expected: String, found: String, }, #[error("Missing attribute: {0}")] MissingAttribute(String), }
-
One-off error messages can be constructed using the
anyhow!
macro, which
supports string interpolation and produces ananyhow::Error
.return Err(anyhow!("Missing attribute: {}", missing));
A
bail!
macro is provided as a shorthand for the same early return.bail!("Missing attribute: {}", missing);
No-std support
In no_std mode, the same API is almost all available and works the same way. To
depend on Anyhow in no_std mode, disable our default enabled «std» feature in
Cargo.toml. A global allocator is required.
[dependencies]
anyhow = { version = "1.0", default-features = false }
Since the ?
-based error conversions would normally rely on the
std::error::Error
trait which is only available through std, no_std mode will
require an explicit .map_err(Error::msg)
when working with a non-Anyhow error
type inside a function that returns Anyhow’s error type.
Comparison to failure
The anyhow::Error
type works something like failure::Error
, but unlike
failure ours is built around the standard library’s std::error::Error
trait
rather than a separate trait failure::Fail
. The standard library has adopted
the necessary improvements for this to be possible as part of RFC 2504.
Comparison to thiserror
Use Anyhow if you don’t care what error type your functions return, you just
want it to be easy. This is common in application code. Use thiserror if you
are a library that wants to design your own dedicated error type(s) so that on
failures the caller gets exactly the information that you choose.
License
Licensed under either of Apache License, Version
2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.
Dependencies
~0–580KB
~12K SLoC