I find myself dealing with error hierarchies that are nested in not-entirely-trivial manners and sometimes involve rather deep nestings. This sometimes requires me to write code such as
match some_operation() {
Err(crate_1::path_1::Error::ErrorFoo(crate_2::path_2::Error::ErrorBar(crate_2::path_2::ErrorBar({source: crate_3::path_3::Error::ErrorSna(_)})))) => {
// Backoff and retry
}
Err(crate_1::path_1::Error::ErrorFoo(crate_2::path_2::Error::ErrorBar(crate_2::path_2::ErrorBar({source: crate_3::path_3::Error::ErrorGa(_)})))) => {
// Actually not an error!
return Ok()
}
// ...
}
This is hard to write, hard to read and bitrots very quickly in case any of the dependencies along the way decides to change its error structure.
Assuming that all the errors along the path implement std::error::Error
, we could turn this into
match some_operation() {
Err(ref err) => {
let err: &dyn std::error::Error = err;
match err.extract() {
Some(crate_3::path_3::Error::ErrorSna(_)) => {
// Backof and retry
}
Some(crate_3::path_3::Error::ErrorGa(_)) => {
// Not really an error
}
_ => // ...
}
}
}
Still not ideal, but much easier to read, write and maintain.
Would there be interest in making something like this part of std
?
Possible implementation:
trait Error {
fn extract<T>(&self) -> Option<&T> where T: 'static + std::error::Error {
let mut cursor = self;
loop {
if let Some(error) = cursor.downcast_ref() {
return Some(error);
}
if let Some(source) = self.source() {
cursor = source;
} else {
return None;
}
}
}
}
(I'm assuming that there are no circular errors, because that would be a scary thing to behold)