Maintaining a registry of all error types

In our codebase we have an error handling scheme which is built on top of downcasting errors to a particular trait that they might implement. This trait provides additional information or context about the error. It works as following:

  1. We declare our error types using failure::Fail derive proc-macro for chaining / Display boilerplate.
  2. We use normal error chaining (currently, via Fail::cause, eventually via Error::source).
  3. In some places, we use a "generic" error type (which is just an alias for failure::Error), so we can put any error in it.
  4. In other places, we use "specific" error types, like one error enum per library crate. Lower-level errors are wrapped into library error type.

On top of that,

  1. We have trait, say, trait ExtraInfo { fn info() -> Info; }, that can provide some extra context information about the error.
  2. We have our own proc-macro to derive implementation of that ExtraInfo. Not all errors implement ExtraInfo, but a lot of them do (ideally, all our errors should implement it).

When error is happening, we would bubble it up the stack, up to the web layer (wrapping it in other errors with additional context). Then we would scan the whole chain of errors and extract that extra information from them, by casting them into that ExtraInfo trait.

This casting is where it gets problematic. For this casting to work, we currently need to "register" all possible error types (so we can check error TypeId and lookup its vtable for ExtraInfo trait).

Currently, we do it manually, by calling each crate "register_errors" function (and it will call its dependencies, and so on). This requires manual work of maintaining these functions, which is not scalable. We currently have ~18 error types, which is not too many, but the main issue is that if you "forget" to register your error, you won't get any feedback. All that would happen is that you would get a less detailed error report in the end, and this is something that is hard to notice when you are writing your code.

I tried other approaches in organizing error types: always using our specific error type; making our error type to be Box<ExtraInfo>, so we can always cast to the same type, etc, but they all seem less ideal to me. I like that in our current implementations errors are just normal Rust errors, you don't need to be aware of these specific details -- except for that registration part.

P.S. The other thing to consider here is that eventually we would need a way to extract all errors we have into a text file of some sort (so we can do localizations, publish official catalog of possible errors, etc).

I wonder if I can use linker script and #[link_section] :thinking:

Okay, it seems like this works (haven't tried it for real yet): Rust Playground

Basically, it's the way to create these nasty static initializers. The idea is that my derive proc-macro will generate these constants in a way that I can iterate through them without explicitly listing them.

1 Like

Is the ctor crate maybe something that could help?

Procedural macro for defining global constructor/destructor functions.
This provides module initialization/teardown functions for Rust (like __attribute__((constructor)) in C/C++) for Linux, OSX, and Windows via the #[ctor] and #[dtor] macros.

I haven't used it myself, just saw it mentioned the other day.

1 Like

Yes, that's exactly what I need! Thanks!