Expand description
Implementation of formatting, to enable colors and the use of box-drawing characters use the
pretty-print
feature.
Note:
error-stack
does not provide any stability guarantees for theDebug
output.
§Hooks
The Debug
implementation can be easily extended using hooks. Hooks are functions of the
signature Fn(&T, &mut HookContext<T>)
, they provide an ergonomic way to partially modify the
output format and enable custom output for types that are not necessarily added via
Report::attach_printable
or are unable to implement Display
.
Hooks can be attached through the central hooking mechanism which error-stack
provides via Report::install_debug_hook
.
Hooks are called for contexts which provide additional values through [Context::provide
] and
attachments which are added via Report::attach
or Report::attach_printable
. The order of
Report::install_debug_hook
calls determines the order of the rendered output. Note, that
Hooks get called on all values provided by [Context::provide
], but not on the Context
object itself. Therefore if you want to call a hook on a Context
to print in addition to its
Display
implementation, you may want to call request.provide_ref(self)
inside of
[Context::provide
].
Hook functions need to be Fn
and not FnMut
, which means they are unable to directly
mutate state outside of the closure.
You can still achieve mutable state outside of the scope of your closure through interior
mutability, e.g. by using the std::sync
module like Mutex
, RwLock
, and atomic
s.
The type, a hook will be called for, is determined by the type of the first argument to the
closure. This type can either be specified at the closure level or when calling
Report::install_debug_hook
.
This type needs to be 'static
, Send
, and Sync
.
You can then add additional entries to the body with HookContext::push_body
, and entries to
the appendix with HookContext::push_appendix
, refer to the documentation of HookContext
for further information.
§Example
use core::fmt::{Display, Formatter};
use std::io::{Error, ErrorKind};
use error_stack::Report;
#[derive(Debug)]
struct ErrorCode(u64);
impl Display for ErrorCode {
fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
write!(fmt, "error: {}", self.0)
}
}
struct Suggestion(&'static str);
struct Warning(&'static str);
// This hook will never be called, because a later invocation of `install_debug_hook` overwrites
// the hook for the type `ErrorCode`.
Report::install_debug_hook::<ErrorCode>(|_, _| {
unreachable!("will never be called");
});
// `HookContext` always has a type parameter, which needs to be the same as the type of the
// value, we use `HookContext` here as storage, to store values specific to this hook.
// Here we make use of the auto-incrementing feature.
// The incrementation is type specific, meaning that `context.increment()` for the `Suggestion` hook
// will not influence the counter of the `ErrorCode` or `Warning` hook.
Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
let idx = context.increment_counter() + 1;
context.push_body(format!("suggestion {idx}: {value}"));
});
// Even though we used `attach_printable`, we can still use hooks, `Display` of a type attached
// via `attach_printable` is only ever used when no hook was found.
Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
context.push_body(format!("error ({value})"));
});
Report::install_debug_hook::<Warning>(|Warning(value), context| {
let idx = context.increment_counter() + 1;
// we set a value, which will be removed on non-alternate views
// and is going to be appended to the actual return value.
if context.alternate() {
context.push_appendix(format!("warning {idx}:\n {value}"));
}
context.push_body(format!("warning ({idx}) occurred"));
});
let report = Report::new(Error::from(ErrorKind::InvalidInput))
.attach_printable(ErrorCode(404))
.attach(Suggestion("try to be connected to the internet."))
.attach(Suggestion("try better next time!"))
.attach(Warning("unable to fetch resource"));
println!("{report:?}");
println!("{report:#?}");
The output of println!("{report:?}")
:
invalid input parameter ├╴at libs/error-stack/src/fmt/mod.rs:58:14 ├╴backtrace (1) ├╴error (404) ├╴suggestion 1: try to be connected to the internet. ├╴suggestion 2: try better next time! ╰╴warning (1) occurred ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ backtrace no. 1 [redacted]
The output of println!("{report:#?}")
:
invalid input parameter ├╴at libs/error-stack/src/fmt/mod.rs:58:14 ├╴backtrace (1) ├╴error (404) ├╴suggestion 1: try to be connected to the internet. ├╴suggestion 2: try better next time! ╰╴warning (1) occurred ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ backtrace no. 1 [redacted] warning 1: unable to fetch resource
§Implementation Details
Nothing explained here is under any semver guarantee. This section explains the algorithm used
to produce the Debug
output of a Report
.
During the explanation we will make use of two different Report
s, the overview tree (shown
first) only visualizes contexts, while the second, more detailed tree shows attachments and
contexts.
In the detailed tree the type of Frame
is distinguished using a superscript letter, ᵃ
is
used to indicate attachments and ᶜ
is used to indicate contexts. For clarity the overview tree
uses digits, while the detailed tree uses letters for different Frame
s.
Overview (Context only) Tree:
0
|
1
/ \
2 6
/ \ |
3 4 7
| |
5 8
Detailed (Context + Attachment) Tree:
Aᶜ
|
Bᵃ
/ \
Cᵃ Eᵃ
| |
Dᶜ Fᵃ
/ \
Gᵃ Iᶜ
|
Hᶜ
During formatting we distinguish between two cases (for contexts):
- Lists
- Groups
in this explanation lists are delimited by [
and ]
, while groups are delimited by (
and
)
.
While formatting we view the Report
s as a tree of Frame
s, therefore the following
explanation will use terminology associated with trees, every Frame
is a node and can have
0..n
children, a node that has no children (a leaf) is guaranteed to be a Context
.
A list is a list of nodes where each node in the list is the parent of the next node in the list
and only has a single child. The last node in the list is exempt of that rule of that rule and
can have 0..n
children. In the examples above, [6, 7, 8]
is considered a list, while [1, 6]
is not, because while 1
is a parent of 6
, 1
has more than 1 child.
A group is a list of nodes where each node shares a common immediate context parent that has
more than 1
child, this means that (2, 6)
is a group (they share 1
as an immediate context
parent), while (3, 4, 6)
is not. (3, 4, 6)
share the same parent with more than 1 child
(1
), but 1
is not the immediate context parent of 3
and 4
(2
) is. In the more detailed
example (Dᶜ, Hᶜ, Iᶜ)
is considered a group because they share the same immediate context
parent Aᶜ
, important to note is that we only refer to immediate context parents, Fᵃ
is the
immediate parent of Iᶜ
, but is not a Context
, therefore to find the immediate context
parent, we travel up the tree until we encounter our first Context
node. Groups always
contain lists, for the sake of clarity this explanation only shows the first element.
The rules stated above also implies some additional rules:
- lists are never empty
- lists are nested in groups
- groups are always preceded by a single list
- groups are ordered left to right
Using the aforementioned delimiters for lists and groups the end result would be:
Overview Tree: [0, 1] ([2] ([3], [4, 5]), [6, 7, 8])
Detailed Tree: [Aᶜ] ([Dᶜ], [Hᶜ], [Iᶜ])
Attachments are not ordered by insertion order but by depth in the tree. The depth in the tree
is the inverse of the insertion order, this means that the Debug
output of all
attachments is reversed from the calling order of Report::attach
. Each context uses the
attachments that are it’s parents until the next context node. If attachments are shared between
multiple contexts, they are duplicated and output twice.
Groups are always preceded by a single list, the only case where this is not true is at the top level, in that case we opt to output separate trees for each member in the group.
§Output Formatting
Lists are guaranteed to be non-empty and have at least a single context. The context is the heading of the whole list, while all other contexts are intended. The last entry in that indentation is (if present) the group that follows, taking the detailed example this means that the following output would be rendered:
Aᶜ
│
╰┬▶ Dᶜ
│ ├╴Bᵃ
│ ╰╴Cᵃ
│
├▶ Hᶜ
│ ├╴Bᵃ
│ ├╴Eᵃ
│ ├╴Fᵃ
│ ╰╴Gᵃ
│
╰▶ Iᶜ
├╴Bᵃ
├╴Eᵃ
╰╴Fᵃ
Groups are visually represented as an additional distinct indentation for other contexts in the preceding list, taking the overview tree this means:
0
├╴Attachment
│
├─▶ 1
│ ╰╴Attachment
│
╰┬▶ 2
│ │
│ ╰┬▶ 3
│ │
│ ╰▶ 4
│ │
│ ╰─▶ 5
╰▶ 6
│
├─▶ 7
│
╰─▶ 8
Attachments have been added to various places to simulate a real use-case with attachments and to visualise their placement.
The spacing and characters used are chosen carefully, to reduce indentation and increase visual legibility in large trees. The indentation of the group following the last entry in the preceding list is the same. To indicate that the last entry in the preceding list is the parent a new indentation of the connecting line is used.
Structs§
- Carrier for contextual information used across hook invocations.
Enums§
- The available supported charsets
- The available modes of color support