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:
- We declare our error types using
failure::Fail
derive proc-macro for chaining / Display boilerplate. - We use normal error chaining (currently, via
Fail::cause
, eventually viaError::source
). - 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. - 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,
- We have trait, say,
trait ExtraInfo { fn info() -> Info; }
, that can provide some extra context information about the error. - We have our own proc-macro to derive implementation of that
ExtraInfo
. Not all errors implementExtraInfo
, 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).