Expand description
§anystack
anystack
is a context-aware error-handling library that supports arbitrary attached user data.
§Overview
anystack
is centered around the idea of building a Report
of
the error as it propagates. A Report
is made up of two concepts:
- Contexts
- Attachments
A context is a view of the world, it helps describe how the current section of code interprets
the error. This is used to capture how various scopes require differing levels of detail and
understanding of the error as it propagates. A Report
always captures the current context
in its generic argument.
As the Report
is built, various pieces of supporting information can be attached. These
can be anything that can be shared between threads whether it be a supporting message or a
custom-defined Suggestion
struct.
§Quick-Start Guide
§In a new project
use anystack::{Report, ResultExt};
// using `thiserror` is not neccessary, but convenient
use thiserror::Error;
// Errors can enumerate variants users care about
// but notably don't need to chain source/inner error manually.
#[derive(Error, Debug)]
enum AppError {
#[error("serious app error: {consequences}")]
Serious { consequences: &'static str },
#[error("trivial app error")]
Trivial,
}
type AppResult<T> = Result<T, Report<AppError>>;
// Errors can also be a plain `struct`, somewhat like in `anyhow`.
#[derive(Error, Debug)]
#[error("logic error")]
struct LogicError;
type LogicResult<T> = Result<T, Report<LogicError>>;
fn do_logic() -> LogicResult<()> {
Ok(())
}
fn main() -> AppResult<()> {
// `anystack` requires developer to properly handle
// changing error contexts
do_logic().change_context(AppError::Serious {
consequences: "math no longer works",
})?;
Ok(())
}
§Where to use a Report
Report
has been designed to be used as the Err
variant of a Result
:
use anystack::{ensure, Report};
fn main() -> Result<(), Report<AccessError>> {
let user = get_user()?;
let resource = get_resource()?;
ensure!(
has_permission(user, resource),
AccessError::PermissionDenied(user, resource)
);
...
}
§Initializing a Report
A Report
can be created directly from anything that implements Error
by using
Report::new()
, IntoReport::into_report()
, or through any of the provided macros
(bail!
, ensure!
).
use std::{fs, io, path::Path};
use anystack::Report;
// Note: For demonstration purposes this example does not use `anystack::Result`.
// As can be seen, it's possible to implicitly convert `io::Error` to `Report<io::Error>`
fn read_file(path: impl AsRef<Path>) -> Result<String, Report<io::Error>> {
let content = fs::read_to_string(path)?;
...
}
§Using and Expanding the Report
As mentioned, the library centers around the idea of building a Report
as it propagates.
§Changing Context
The generic parameter in Report
is called the current context. When creating a new
Report
, the Error
that’s provided will be set as the current context. The current
context should encapsulate how the current code interprets the error. As the error propagates,
it will cross boundaries where new information is available, and the previous level of detail is
no longer applicable. These boundaries will often occur when crossing between major modules, or
when execution crosses between crates. At this point the Report
should start to operate in a
new context. To change the context, Report::change_context()
is used:
(Again, for convenience, using ResultExt
will do that on the Err
variant)
use core::error::Error;
use anystack::{Report, ResultExt};
#[derive(Debug)]
struct ParseConfigError;
impl fmt::Display for ParseConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("could not parse configuration file")
}
}
// It's also possible to implement `Error` instead.
impl Error for ParseConfigError {}
// For clarification, this example is not using `anystack::Result`.
fn parse_config(path: impl AsRef<Path>) -> Result<Config, Report<ParseConfigError>> {
let content = fs::read_to_string(path.as_ref())
.change_context(ParseConfigError)?;
...
}
§Building up the Report - Attachments
Module/crate boundaries are not the only places where information can be embedded within the
Report
however. Additional information can be attached within the current context, whether
this be a string, or any thread-safe object. These attachments are added by using
Report::attach()
and Report::attach_printable()
:
struct Suggestion(&'static str);
fn parse_config(path: impl AsRef<Path>) -> Result<Config, Report<ParseConfigError>> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.change_context(ParseConfigError::new())
.attach(Suggestion("use a file you can read next time!"))
.attach_printable_lazy(|| format!("could not read file {path:?}"))?;
Ok(content)
}
As seen above, there are ways on attaching more information to the Report
: attach
and
attach_printable
. These two functions behave similar, but the latter has a more restrictive
bound on the attachment: Display
and Debug
. Depending on the function used, printing the
Report
will also use the Display
and Debug
traits to describe the attachment.
This outputs something like:
#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/lib__suggestion.snap")))]
The Suggestion
which was added via attach
is not shown directly and only increases the
counter of opaque attachments for the containing Error
. The message which was passed to
attach_printable
, however, is displayed in full. To be able to show attachments that have
been added via attach
, one must make use of hooks instead.
§Multiple Errors
Report
provides native support for combining and propagating multiple errors. This feature
is particularly useful in scenarios such as parallel processing, where multiple errors might
occur independently. In these cases, you can utilize the Extend
trait implementation and the
push()
method to aggregate and propagate all encountered errors, rather than just a single
one.
anystack is designed to be explicit about the presence of single or multiple current contexts. This distinction is reflected in the generic type parameter:
Report<C>
indicates that a single current context is present.- [
Report<[C]>
] signifies that at least one current context is present, with the possibility of multiple contexts.
You can seamlessly convert between these representations using Report::expand
to transform
a single-context report into a multi-context one. Using Report::change_context
will
transform a [Report<[C]>
] to a Report<C2>
, where C2
is a new context type.
fn parse_configs(paths: &[impl AsRef<Path>]) -> Result<Vec<Config>, Report<[std::io::Error]>> {
let mut configs = Vec::new();
let mut error: Option<Report<[std::io::Error]>> = None;
for path in paths {
let path = path.as_ref();
match fs::read_to_string(path) {
Ok(ok) => {
configs.push(ok);
}
Err(err) => {
if let Some(error) = error.as_mut() {
error.push(Report::from(err));
} else {
error = Some(Report::from(err).expand());
}
}
}
}
if let Some(error) = error {
return Err(error);
}
Ok(configs)
}
§In-Depth Explanation
§Crate Philosophy
This crate adds some development overhead in comparison to other error handling strategies,
especially around creating custom root-errors (specifically anystack
does not allow using
string-like types). The intention is that this reduces overhead at other parts of the process,
whether that be implementing error-handling, debugging, or observability. The idea that
underpins this is that errors should happen in well-scoped environments like reading a file or
parsing a string into an integer. For these errors, a well-defined error type should be used
(i.e. io::Error
or ParseIntError
) instead of creating an error from a string. Requiring a
well-defined type forces users to be conscious about how they classify and group their
custom error types, which improves their usability in error-handling.
§Improving Result::Err
Types
By capturing the current Error
in the type parameter, return types in function signatures
continue to explicitly capture the perspective of the current code. This means that more often
than not the user is forced to re-describe the error when entering a substantially different
part of the code because the constraints of typed return types will require it. This will happen
most often when crossing module/crate boundaries.
An example of this is a ConfigParseError
when produced when parsing a configuration file at
a high-level in the code vs. the lower-level io::Error
that occurs when reading the file from
disk. The io::Error
may no longer be valuable at the level of the code that’s handling parsing
a config, and re-framing the error in a new type allows the user to incorporate contextual
information that’s only available higher-up in the stack.
§Compatibility with other Libraries
anystack
uses the standard Error
type which makes it compatible with almost all other
libraries that use that trait.
This has the added benefit that migrating from other error libraries can often be incremental,
as a lot of popular error library types will work within the Report
struct.
In addition, anystack
supports converting errors generated from the anyhow
or eyre
crate via IntoReportCompat
.
§Doing more
Beyond making new Error
types, the library supports the attachment of arbitrary
thread-safe data. These attachments can be
requested through Report::downcast_ref()
or Report::downcast_iter()
.
This gives a novel way to expand standard error-handling approaches,
without decreasing the ergonomics of creating the actual error
variants:
fn main() {
if let Err(report) = parse_config("config.json") {
for suggestion in report.downcast_iter::<Suggestion>() {
eprintln!("suggestion: {}", suggestion.0);
}
}
}
§Additional Features
The above examples will probably cover 90% of the common use case. This crate does have additional features for more specific scenarios:
§Automatic Backtraces
Report
will try to capture a Backtrace
if RUST_BACKTRACE
or RUST_BACKTRACE_LIB
is
set and the backtrace
feature is enabled (by default this is the case).
Unlike some other approaches, this does not require the user modifying their custom error types to be aware of backtraces, and doesn’t require manual implementations to forward calls down any wrapped errors.
§No-Std compatible
The complete crate is written for no-std
environments, which can be used by setting
default-features = false
in Cargo.toml.
§Macros for Convenience
Two macros are provided to simplify the generation of a Report
.
bail!
acts like callingIntoReport::into_report()
but also immediately returns theReport
asErr
variant.ensure!
will check an expression and if it’s evaluated tofalse
, it will act likebail!
.
§Span Traces
The crate comes with built-in support for tracing
s SpanTrace
. If the spantrace
feature
is enabled and an ErrorLayer
is set, a SpanTrace
is either used when provided by the
root Error
or will be captured when creating the Report
.
§Debug Hooks
One can provide hooks for types added as attachments when the std
feature is enabled. These
hooks are then used while formatting Report
. This functionality is also used internally by
anystack
to render Backtrace
, and SpanTrace
, which means overwriting and
customizing them is as easy as providing another hook.
You can add new hooks with Report::install_debug_hook
. Refer to the module-level
documentation of fmt
for further information.
§Additional Adaptors
ResultExt
is a convenient wrapper around Result<_, impl Error>
and Result<_, Report<impl Error>
. It offers attach
and
change_context
on the Result
directly, but also a lazy
variant that receives a function which is only called if an error happens.
In addition to ResultExt
, this crate also comes with FutureExt
, which provides the same
functionality for Future
s.
§Colored output and charset selection
You can override the color support by using the Report::set_color_mode
. To override the
charset used, you can use Report::set_charset
. The default color mode is emphasis.
The default charset is UTF-8
.
To automatically detect support if your target output supports unicode and colors you can check
out the detect.rs
example.
§Feature Flags
Feature | Description | default |
---|---|---|
std | Enables support for Error | enabled |
backtrace | Enables automatic capturing of Backtrace s | enabled |
spantrace | Enables automatic capturing of SpanTrace s | disabled |
hooks | Enables hooks on no-std platforms using spin locks | disabled |
serde | Enables serialization support for Report | disabled |
anyhow | Provides into_report to convert anyhow::Error to Report | disabled |
eyre | Provides into_report to convert eyre::Report to Report | disabled |
§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.
Modules§
- fmt
- Implementation of formatting, to enable colors and the use of box-drawing characters use the
pretty-print
feature. - future
- Extension for convenient usage of
Report
s returned byFuture
s. - iter
- Iterators over
Frame
s.
Macros§
- bail
- Creates a
Report
and returns it asResult
. - ensure
- Ensures
$cond
is met, otherwise return an error. - report
Deprecated - Creates a
Report
from the given parameters.
Structs§
- Frame
- A single context or attachment inside of a
Report
. - Report
- Contains a
Frame
stack consisting ofContext
s and attachments.
Enums§
- Attachment
Kind - Classification of an attachment which is determined by the method it was created in.
- Frame
Kind - Classification of the contents of a
Frame
, determined by how it was created.
Traits§
- Context
Deprecated - Defines the current context of a
Report
. - Future
Ext - Extension trait for
Future
to provide contextual information onReport
s. - Into
Report - Provides unified way to convert an error-like structure to a
Report
. - Into
Report Compat - Compatibility trait to convert from external libraries to
Report
. - Result
Ext - Extension trait for
Result
to provide context information onReport
s.
Type Aliases§
- Result
Deprecated - Result
Stack Result<T, Error>