Expand description
§Stack Error
Stack Error reduces the up-front cost of designing an error handling solution for your project, so that you focus on writing great libraries and applications. Stack Error has three goals:
- Provide ergonomics similar to
anyhow
. - Create informative error messages that facilitate debugging.
- Provide typed data that facilitates runtime error handling.
§Overview
-
Build informative error messages for debugging with minimal effort. The error message is co-located with the error source, which helps document your code.
use stackerror::prelude::*; pub fn process_data(data: &str) -> StackResult<String> { let data: Vec<String> = serde_json::from_str(data) .map_err(StackError::from_msg) .stack_err_msg(fmt_loc!("data is not a list of strings"))?; data.first() .cloned() .ok_or_else(StackError::new) .with_err_msg(fmt_loc!("data is empty")) }
In this example, when the data isn’t a valid JSON vector,
StackError::from_msg
is used to create a new error using serde’s error message, and theStackError::stack_err_msg
method is used to stack a new error onto this with a custom message. Thefmt_loc
marco prepends the file name and line number to the message. The resulting error message is:Error: EOF while parsing a value at line 1 column 0 src/main.rs:6 data is not a list of strings
If the data is an empty JSON vector, then
StackError::new
is used to create an empty error, andStackError::with_err_msg
is used to set the error’s message. -
Handle errors at runtime by inspecting an optional error code.
let data = if data.err_code() == Some(&ErrorCode::HttpTooManyRequests) { // retry } else { data? };
ErrorCode
includes HTTP error codes,std::io::ErrorKind
codes, and a handful of runtime codes to cover non-HTTP and non-IO cases. You can derive your own error codes as described later in the examples. -
Easily define your own error type. This allows you to customize error codes, derive
From
implementations, and provide a single opaque error type to downstream code. -
Provides error types that implement
std::error::Error
. Errors are compatible with the broader Rust ecosystem.
§Rationale
The Rust error handling ecosystem is largely built on two libraries:
anyhow
and thiserror
.
Stack Error aims to explore the space between these two libraries:
providing ergonomic error handling and an error type that is suitable for library development.
§Anyhow comparison
Using anyhow
makes development quick as error handling is nearly always just a matter of adding the ?
operator.
But this can slow down the debugging process.
Consider this example:
use serde::Deserialize;
use anyhow::Result;
#[derive(Debug, Deserialize)]
struct Config {
key: String,
}
fn print_config(data: &str) -> Result<()> {
let config: Config = serde_json::from_str(data)?;
println!("{:?}", config);
Ok(())
}
fn main() -> Result<()> {
print_config(r#"{"key": "value", "invalid"}"#)?;
Ok(())
}
The resulting error message is: Error: expected `:` at line 1 column 27
. The message clearly states what went wrong, but not how it went wrong: what was the program decoding, and why is the value needed. Running with RUST_BACKTRACE=1
prints a backtrace that can answer these questions, though it contains nearly 20 unrelated frames. Debugging this example is feasible, but a bit cumbersome.
Handling all errors with the ?
operator speeds up the writing process, but can hinder the overall development process by making debugging more difficult. anyhow
offers an alternative: the anyhow::Context
trait.
Using the context
method helps provide clear error messages that answer what went wrong and how it went wrong. It also helps document the code by co-locating error sources with their corresponding error messages. This is the inspiration for Stack Error’s stack_err
method.
As a an application grows, the distinction between application and library can become blurred as modules are introduced to support the application code. You might eventually find you want to handle some errors. You can use anyhow::Error::downcast
, but this is cumbersome as you need to try to downcast to every possible error type.
§Thiserror comparison
The thiserror
library provides flexible tools to facilitate the creation of custom error types. There is no one way to use it, so it’s hard to make a direct comparison other than to say: Stack Error is opinionated and aims to be minimal but generally useful. This reduces the effort required to develop (and stick to) a good error handling strategy for your project. By contrast, when using thiserror
, you have to make many decisions at the start of a project about where and how to define your errors. And if you don’t put much thought into the design of your errors, you could end up:
- Constantly duplicating error messages in enum variant names and error message strings;
- Creating generic errors that result in poor debugging messages and insufficient runtime information for error handling;
- Creating too many errors exposing the internals of your library or application, making it hard to refactor;
- Having to move back-and-forth between writing code and defining error variants in separate files.
§Library structure
The core of the library is the StackError
struct and the ErrorStacks
trait. The ErrorCode
enum can be used to add error codes to any
ErrorStacks
.
The ErrorStacks
methods are implemented on StackError
and
Result<_, StackError>
. So you can act on the StackError
inside a Result
.
The methods have no impact on the result if it is an Ok
variant.
Typically, you will access these using the prelude
module which also defines StackResult
.
§Custom error type
Create your error type by using the derive_stack_error
macro:
// src/errors.rs
pub use stackerror::prelude::*;
#[derive_stack_error]
struct LibError(StackError);
pub type LibResult<T> = std::result::Result<T, LibError>;
Then you can replace use stackerror::prelude::*
with use crate::errors::*
in your code in your code, and use LibError
and LibResult
.
This has several benefits:
- Your code exposes a single opaque error type. Downstream error-handling code
doesn’t need to know about
StackError
, and can writeFrom
implementations that handle any error returned by your code. - Debug messages include only errors from your codebase since you can stack
only instances of
LibError
. - You can customize the error codes.
§Examples
You can build a new error with an error message that is std::fmt::Display
:
use stackerror::prelude::*;
fn process_data() -> StackResult<()> {
Err(StackError::from_msg("failed to process data"))
}
You can include file and line information in error messages using the fmt_loc!
macro:
use stackerror::prelude::*;
fn process_data() -> StackResult<()> {
Err(StackError::from_msg(fmt_loc!("failed to process data")))
}
You can include optional error handling information:
use stackerror::prelude::*;
fn process_data() -> StackResult<()> {
Err(
StackError::from_msg(fmt_loc!("failed to process data"))
.with_err_code(ErrorCode::HttpImATeapot)
.with_err_uri("https://example.invalid/teapot")
)
}
You can chain errors together to provide context in the error message:
use stackerror::prelude::*;
pub read_data() -> StackResult<String> {
Err(StackError::from_msg(fmt_loc!("failed to read data")))
}
pub process_data() -> StackResult<()> {
// NOTE: stack_err can be called directly on the Result
read_data().stack_err_msg(fmt_loc!("failed to process data"))
}
This would result in an error message like:
src/main:9 failed to process data
src/main:4 failed to read data
You can use your own error codes by defining an ErrorCode
type in the scope where derive_stack_error
is used:
use stackerror::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ErrorCode {
SomethingWentWrong,
SomethingElseWentWrong,
}
#[derive_stack_error]
struct ErrorWithCustomCodes(StackError);
Re-exports§
pub use prelude::*;
Modules§
- codes
- Provides the
ErrorCode
enum. - error
- Provides the
StackError
struct which implements theErrorStacks
trait. - macros
- Provides a macro for formatting error messages with file and line information.
- prelude
- Provides re-exports for commonly used types and traits, and defines the
StackResult
type.
Macros§
- fmt_loc
- Formats a string using
format!
, and prefixes it with the file name and line number.