Module error_stack::fmt

source ·
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 the Debug 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 atomics.

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 Reports, 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 Frames.

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 Reports as a tree of Frames, 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