Skip to content
This repository has been archived by the owner on Aug 16, 2021. It is now read-only.

[Question] Custom context in errors #197

Closed
idubrov opened this issue May 2, 2018 · 2 comments
Closed

[Question] Custom context in errors #197

idubrov opened this issue May 2, 2018 · 2 comments

Comments

@idubrov
Copy link

idubrov commented May 2, 2018

I have a requirement that any error chain produced by our system could be serialized into a special "error list" format (basically, a JSON with some standardized fields).

In an ideal world, Fail trait would magically contain a function fn error_info() -> ErrorInfo that I could use to extract information from each error in the chain and serialize it into the list.

There is already a functionality to traverse error chain (fn causes(&self) on Fail), so I will be able to traverse the chain, get those trait objects &Fail for each error and grab the info.

However, I cannot add my custom function to failure::Fail (duh!) and I don't want to design my own error crate.

Another requirement is that I want to have "normal" error enums, in the sense that each library crate of my system would just declare its own error type and make it derive Fail. I don't want some special "error" type used everywhere, etc.

Also, I want to be able to build those chains by using functionality similar to what Context does in failure.

The biggest stumbling block for me now is that how do I do "dynamic" dispatching while traversing that chain? Currently I built a contraption around Context<T> that uses my custom error type:

for cause in error.causes() {
  if let Some(err) = cause.downcast_ref::<MyError>() {
    let info: ExtraInfo = err.get_extra_info();
    infos.push(info)
  }
}

I'll try to explain it, but it's a little bit weird. And it does not work exactly the way I want it.

  • I have my custom trait ExtraInfo
  • All my error types implement it
  • I use my custom error type MyError, which contains a Box<ExtraInfo>
  • Context<T> where T: ExtraInfo is convertible to MyError.

In the end, this extra info scavenging works only if I convert each error into MyError first. So, for example, simply wrapping error and using #[cause] wouldn't work because wrapped error is not "downcastable" to MyError.

You can see that I'm using my custom errors sometimes as context to Context<T> and sometimes as errors themselves, which is awkward (but I think this is similar to the https://boats.gitlab.io/failure/error-errorkind.html ?).

However, I think, I do want some flexibility in the way I chain errors. Sometimes, I want an explicit wrapping, like here:

pub MyHighError {
  ...
  #[fail(display = "low-level error happened!")]
  LowError(#[cause] MyLowError),
  ...
}

Sometimes, I want to use literal from MyHighErroras a context to the lower-level error:

pub MyHighError {
  ...
  #[fail(display = "low-level error happened!")]
  LowError,
  ...
}

let result = low_level_operation().context(MyHighError::LowError)?;

The former is better suited for libraries (more traditional error handling with specific error types), the latter for the application itself (don't really care about exact error type, fine with just using generic MyError).

I think, I took the wrong turn and what I really want is what I described in the beginning: I want each Fail implementation to optionally provide custom info I need. Basically, I want it to be "castable" to another trait of mine.

I can create such system by making a global map that maps from error type id into virtual table for my own ExtraInfo trait, but I would rather use Rust code to match on interface type versus static global mutable map (mutable, because it needs to be initialized from multiple parts of the system to "register" mappings between type ids into trait "virtual tables").

What I'm asking here, would it make any sense to expand "downcasting" functionality in Fail to support downcasting to traits, by adding the following function to the Fail:

unsafe fn cast_iface(&self, iface: TypeId) -> Option<std::raw::TraitObject>;

The implementation would be a match on iface type id, somehow generated automatically:

impl Fail for MyCustomErrorType {
  unsafe fn cast_iface(&self, iface: TypeId) -> Option<TraitObject> {
    if iface == TypeId::of::<ExtraInfo>() {
      return Some(std::mem::transmute(self as &ExtraInfo));
    }
    ...
    None
  }
}

(although, I don't know how to combine this with derive for Fail? should user specify which traits error should be "castable" to?)

Then, this function could be used like err.cast_iface<ExtraInfo> to get TraitObject, which could be transmuted later into &ExtraInfo, by some safe wrapper (which, presumably, should be auto-generated), like below:

pub trait ExtraInfoCast: Fail {
  fn as_extra_info(&self) -> Option<&ExtraInfo> {
    unsafe {
      self.cast_iface(TypeId::of::<Definitions>()).map(|v| std::mem::transmute(v))
    }
  }
}
impl ExtraInfoCast for Fail {}

Does it make any sense? Any other suggestions?

@idubrov idubrov changed the title Custom context in errors [Question] Custom context in errors May 3, 2018
@idubrov
Copy link
Author

idubrov commented Jun 6, 2018

TL;DR; what I'm proposing here is extending Fail trait to support to downcasting to a trait.

I have a working prototype which is essentially an Any able to downcast to a trait (with the requirement that all supported interfaces are explicitly listed).

@idubrov
Copy link
Author

idubrov commented May 4, 2020

Amazing, seems like this RFC is along the same lines!

@idubrov idubrov closed this as completed May 4, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant