Ergonomics of deeply nested error hierarchies

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)

You may be interested in the error_iter nightly feature with which you could write extract as

#![feature(error_iter)]

use std::error::Error;

pub fn extract<'a, T>(err: &'a (dyn Error + 'static)) -> Option<&'a T>
where
    T: Error + 'static,
{
    err.sources().find_map(<dyn Error>::downcast_ref)
}

anyhow also provides a similar method as anyhow::Error::chain.

There's also some related discussion at Return the custom error instead of its cause in `io::Error::{cause,source}` by thomcc · Pull Request #101818 · rust-lang/rust · GitHub regarding a version of Error::is that iterates through the source errors, so I think there might be interest in an equivalent for downcast_ref.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.