(apologies for the double post, but very distinct content.)
To add a little extra context to this, in the rare case where I've I've written proper "application code" that has enough layers doing more than just script-style plumbing and could benefit from attaching a higher level context stack I typically use tracing for structured log capture and function instrumentation, and notably tracing-error to capture a high-level structured span trace in my application error reports (typically via color-eyre), which automatically captures structured context from the functions annotated with #[tracing::instrument]
. This information is both significantly more immediately relevant and more useful than a backtrace (including function argument values), and eliminates most need to manually provide .context()
except when capturing a library error into an eyre error report.
I also only really have worked on solo projects (I can usually trust myself to use impl Error
(thiserror-y) error types for reasonably recoverable tail-ish errors and error reports (anyhow-y) when the error starts propagating between application layers) or already established projects (which have an established big-picture error handling pattern already), so to repeat, I'm likely not the intended audience/consumer for error-stack. To my sensibility, error-stack seems primarily aimed at the multi-team application stack use case where a) maintaining a manual cross-layer error context stack is beneficially more useful than an automatic stack backtrace or log spantrace, and b) enforcing the use of error-stack contexts in in-house code is an easier policy to maintain than trying to specify policy for how/when to define custom error enums versus bubble an existing error type versus use an application error report type.
If anything, I would liken it to having a "colored" anyhow; certain module/crate scopes within the larger application define their error context (e.g. as you might a thiserror aggregate error for the scope), and within that scope it's treated like an anyhow error, attaching dynamic context, but when you move to a different major context, the compiler will ensure you remember to add that context information in order to match the new report "color." It's less like a stack of "library" (thiserror-ish) errors, and more like a stack of "application" (anyhow-ish) errors. You get the convenience of anyhow within one context but the strictness of thiserror when switching contexts.
I also suppose that working with enum LibErrorKind { Io, Format, Other }
can be easier than with enum LibError { Io(io::Error), Format(serde_format::Error), Other(Box<dyn 'static + Error>) }
. Plus the benefit of having a stack-small (single nonnull pointer) report type to propagate doesn't go unappreciated; Rust code typically isn't particularly copy-efficient by default. (As opposed to C++ which ime on average ends up encouraging allocating more and copy/moving objects less than equivalent Rust code written at a similar level of idiomatic abstraction.)
As with most things, it depends on context. If you have a library/abstraction boundary where most errors can be produced by most fallible operations (the quintessential example being that IO that seems local might actually entail an internet operation, or any other kind of context mixing), then using a single uniform error is preferable to trying to capture subtly different sets of potential errors from the same pool. Similarly, high-level convenience APIs that bundle multiple individually accomplishable fallible steps together generally benefit from using a shared high-level bundled error type instead of more specific ones; the caller is generally just interested in that you failed rather than how you failed, so making dispatch on how you failed slightly less convenient in order to simplify the API can be useful.
For many other cases, it's much preferable to have a specific error kind. Generally when a) there's a relatively small set of potential ways an operation could fail, b) in which of those ways you failed is potentially relevant to how the caller can address the failure, and c) the pool of potential failures is mostly distinct from how other code might fail.
So, I guess, use your best judgement. Add as much context as reasonably possible without significantly hurting your downstream usability or making your own code unmaintainable. (And also maybe refrain from embedding context which the caller provides by reference, to avoid causing unneeded clones when the caller still has that context.)