-
Notifications
You must be signed in to change notification settings - Fork 140
Should we recommend middle-layer libraries add context to errors? #6
Comments
I might just be restating the question, but when do you use error_chain! {
errors {
Io(op: &'static str, path: PathBuf)
}
}
let out = File::create(&output_file).chain_err(|| Io("create", output_file.clone()))?; So the context is in the variant and the original |
I think what you're recommending is I should do (assuming your let out = File::open("Cargo.toml").context(ErrorKind::InvalidCargoToml)?; Is that right? It seems pretty similar to propagating an error-chain error, really. Except you could end up with a chain of contexts instead of a chain of causes. |
Full code sample for a "context adding error," that guidance will recommend for complex, intermediate libraries that want to provide the most robust error information: use std::fmt::{self, Debug, Display};
use failure::{Fail, Context};
pub struct MyError {
inner: Context<MyErrorKind>,
}
impl MyError {
fn new(kind: MyErrorKind) -> MyError {
MyError { inner: Context::new(kind) }
}
pub fn kind(&self) -> MyErrorKind {
*self.inner.get_context()
}
}
impl Fail for MyError {
fn cause(&self) Option<&Fail> {
self.inner.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.inner, f)
}
}
impl Debug for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(&self.inner, f)
}
}
impl From<Context<MyErrorKind>> for MyError {
fn from(inner: Context<MyErrorKind>) -> MyError {
MyError { inner }
}
}
#[derive(Copy, Debug, PartialEq, Eq, Fail)]
enum MyErrorKind {
// C-style enum
} You can throw these errors with the use failure::ResultExt;
do_some_io().context(MyErrorKind::WhatThisErrorMeans)? It allows a many-to-many relationship between the errors at the level of your library (enumerated with "MyErrorKind") and the underlying error types of errors your dependencies throw. Users can access both with Probably we will eventually want a way to generate this code. I can think of a few different syntaxes: #[derive(Fail)]
#[fail(context_wrapper)]
struct MyError {
inner: Context<MyErrorKind>,
}
context_wrapper!(MyError <= MyErrorKind); Going to punt on codegen for this pattern for 0.1. |
Added guidance about this pattern at https://boats.gitlab.io/failure/error-errorkind.html |
I am trying to implement an My errors look like this (it's actually a bit more complicated, because use i2cdev::linux::LinuxI2CError;
#[derive(Clone, Copy, Fail, Debug)]
#[fail(display = "My error occured")]
pub struct OutputLevelError {};
#[derive(Debug, Fail)]
enum MyCombinedError {
I2CError(LinuxI2CError),
MyError,
} Now underneath are What is the suggested solution in how I attack those? The way it currently stands I feel like I would need to match on the various errors kinds IO and Nix and provide extra context for each single one. But that appears silly. |
The idea of this pattern was to insert information about what failed at the level of abstraction you're writing - this shouldn't depend on the underlying cause of the LinuxI2CError. As a classic example: you have a function which reads some manifest. You wrap all of the errors in the context which ends up printing But no matter what the underlying error was, at your level of abstraction the action which failed is the same: you couldn't process the manifest. Its possible that you need different context depending on variants of the underlying error, but this pattern isn't really trying to solve that. I'm not sure if you actually do, though - don't the variants convey sufficient information about the errors at their level of abstraction? |
I think my question stems from me not completely understanding how the library and this pattern work. This statement of yours makes my misunderstanding clear to me:
I think I am starting to understand how the library works, and that I had the impression that I would lose those errors and replace them by less information. |
To be clear, the Display impl ( So you have a choice when displaying the error to:
Depending on how the error is being printed, any of these may be appropriate. |
For "leaf" libraries and "root" applications the story from failure is pretty clear - leaf libraries should define new types that implement
Fail
, and applications should use theError
type (unless they are no_std applications).But what about the in-between space? What if you depend on a bunch of libraries with their own error types, and introduce a few of your own as well?
There are a few options I know about:
Error
. In many cases, this is very convenient. The downsides are that you require theError
type (making it inappropriate to call these functions when failure could be in the hot path) and that users get no additional context about what the error means in the context of your library - libfoobar threw anio::Error
, what does that mean??
conversion), and when users are trying to downcast they now have to deal with all of this additional context information.As an example of how to use
Context<D>
from failure to do the contextual form:You can now use the
.context()
method to add anErrorKind
to an underlying error like an io::Error, a parsing Error, or an error in git.I think 1 and 2 are both broadly applicable depending on the use case, whereas 3 (what error-chain encourages today) is rarely a good idea (basically, only if you can't afford to do the allocation in
Error
). But when do we recommend 1 over 2, or vice versa? How do users know whether they should introduce a contextual construct?The text was updated successfully, but these errors were encountered: