Passing errors through library runtimes

I'm not quite sure I have a question, I may just need to do a brain-dump to collect my thoughts. (Edit: I do have a question, or two).

There are library crates that act as wrappers around applications. When using such libraries, the application's main() calls some runtime, which in turn calls an application callback (could be a closure or a trait method), which in a sense is the "real" application entry point:

impl SomeRuntimeWrapper for MyApp {
  fn app_main(&self) {
    // the actual application code goes here
  }
}

fn main() {
  let app = MyApp { };
  let res = wrapper::run_app(app);
}

It's basically a stack:

+------------+
|    app     |
+------------+
|  wrapper   |
+------------+
| app main() |
+------------+

... where app main() and app are the same runtime, but they are separated by the wrapper. If an error occurs in app, one might want to bubble the error through wrapper and back into app main(). The problem is that wrapper has no way to know about app beforehand, so it can't just pass it's app::Error type.

As far as I know, there are two ways to solve this (ignoring things like using globals, just talking about actually passing the error type through the wrapper runtime):

  • Generics
  • RTTI (aka Any in Rust)

I'm a prolific user of generics for such a thing. My wrapper libraries would have:

pub enum Error<E> {
  // ...
  App(E),
  // ...
}

.. where the App(E) case is intended for the application to pass errors back (from its "inside") to itself (on the "outside"). I.e. in the application, E would be app::Error.
The wrapper's public interfaces that could kick off the application runtime would return Result<(), wrapper::Error<E>>.

I know that one complaint about generics is that they can become viral. Turns out that if you put a generic on the library's Error type, then it's going everywhere. Honestly, the virality itself isn't a problem to me. However, eventually I ended up in strange place where I needed to add a PhantomData to an enum, and the workarounds were ... let's just say that this is when I started looking at using dynamic types instead.

With RTTI the wrapper uses this instead:

pub enum Error {
  // ...
  App(Box<dyn Any>),
  // ...
}

(Well not exactly, there are some bounds in there as well).

The application checks if a call to the wrapper returned Err(wrapper::Error::App()), and if it is then it needs to take the error out and attempt to cast it to app::Error.

The nice thing about using generics is that the correct error type is enforced at the call site that generates the error at compilation time. Using RTTI makes the receiver check the type at runtime, which is a little annoying because there needs to be added error handling to handle the case where someone put an unsupported error type in there. (I once accidentally passed a Result instead of an Error and this problem went undetected for quite a while, which was rather annoying -- this is the sort of thing I'd like the compiler to tell me about).

I guess I'm wondering if there are any other ways to pass errors through a runtime? In particular, are there any methods that would be a little more of a middle-ground (no PhantomData in enums, but make it a little more difficult to pass wrong error type through the wrapper runtime)?

I had some crazy idea about using RTTI, but use trait-bounds to make sure that only a app::Error could be passed to wrapper::Error::App(). Not sure how this would work exactly though.

I guess the wrapper's Error::App() could use some type that must be constructed using a constructor that requires a marker trait bound. That way the application must "bless" types that should be able to be used in wrapper::Error::App().

Am I overthinking this? To be honest, the bug I mentioned above, where I accidentally put in a Result instead of just the Error spooked me. These are some of the primary reasons I started using Rust -- to get away from these kinds of bugs. I want the type system to disallow these types of mistakes at compile time, which is why I'm not a huge fan of runtime type checking.

I have two suggestions:

  1. Box<dyn Error> - it's RTTI but you enforce at compile time that the type is an error
  2. anyhow which is like Box<dyn Error> but with superpowers
1 Like

In my design opinion, the best fix is here. Using a single error type for an entire library is an unnecessary constraint. If nothing else, you should have a separate error type for everything that can happen in the main loop, including user errors. Any functions not part of the main loop itself can use more specific, non-generic error types, which can be wrapped by the main loop error type as necessary.

pub enum MainError<E> {
    Foo(FooError),
    Bar(BarError),
    Custom(E),
}
1 Like

One of the (thankfully rare) cases is where the error type actually borrows something which prevents it from bubbling up all the way to main. So in some cases I've ended up with a unitary error variant(s)...

Also if you end up with a bunch of generics it can help to just add an Error associated type to e.g. the SomeRuntimeWrapper trait (or somewhere), so you don't have to pass a generic E along side bunches of other things.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.