rustdowncastanyhow

How to recover the original error type from Box<dyn StdError> after converting from anyhow::Error?


I'm trying to recover the original error type SqlxError after converting it to anyhow::Error and then boxing it as a Box<dyn std::error::Error + Send + Sync + 'static>. However, the type information seems to be erased, and downcasting fails.

use std::error::Error as StdError;
use std::fmt;

#[derive(Debug)]
struct SqlxError {
    message: String,
}

impl fmt::Display for SqlxError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "SQLx Error: {}", self.message)
    }
}

impl StdError for SqlxError {}

fn main() {
    // Create sqlx::Error
    let sqlx_error = SqlxError {
        message: "Database connection failed".to_string(),
    };

    // Convert to anyhow::Error
    let anyhow_error = anyhow::Error::new(sqlx_error);

    // Convert to Box<dyn StdError>
    let boxed_error: Box<dyn StdError + Send + Sync + 'static> = anyhow_error.into();

    // Now try to recover
    let recovered_error = anyhow::Error::from_boxed(boxed_error);
    // work well! output: SqlxError { message: "Database connection failed"} 
    dbg!(&recovered_error);
    if let Some(sqlx_err) = recovered_error.downcast_ref::<SqlxError>() {
        println!("Successfully downcast to SqlxError: {:?}", sqlx_err);
        println!("Error message: {}", sqlx_err);
    } else {
        println!("Failed to downcast to SqlxError");
        println!("Actual error: {}", recovered_error);
    }
}

As seen in the dbg! output below, the data is still an SqlxError, but downcast_ref::<SqlxError>() fails.

[src/main.rs:31:5] &recovered_error = SqlxError {
    message: "Database connection failed",
}

My understanding is that converting anyhow::Error into Box<dyn StdError> erases the anyhow wrapper and any ability to later recover the original type through anyhow::Error.

Is there any way to recover the original error type SqlxError from the boxed trait object after converting from anyhow::Error?


Solution

  • Calling from_boxed does not "recover" the anyhow Error from the Box<dyn Error>, it just wraps the error in another anyhow Error. So you'd end up with something like this:

    recovered_error = anyhow::Error {
        Box<dyn Error> {
            anyhow::Error {
                SqlxError
            }
        }
    }
    

    Reading through the docs, the problem starts with anyhow_error.into(), it is equivalent to calling .into_boxed_dyn_error() which says:

    If a backtrace was collected during construction of the anyhow::Error, that backtrace remains accessible using the standard library Error trait’s provider API, but as a consequence, the resulting boxed error can no longer be downcast to its original underlying type.

    So you can't recover SqlxError using this method; the actual type behind the Box<dyn Error> is an anyhow internal type that you have no way to access concretely.

    There is another method called .reallocate_into_boxed_dyn_error_without_backtrace() which says:

    Unlike self.into_boxed_dyn_error(), this method relocates the underlying error into a new allocation in order to make it downcastable to &E or Box<E> for its original underlying error type. Any backtrace collected during construction of the anyhow::Error is discarded.

    So if you need to be able to recover the original error, you'd need to use the latter. Then you downcast directly to SqlxError via the standard Box<dyn Error>:

    let boxed_error = anyhow_error.reallocate_into_boxed_dyn_error_without_backtrace();
    
    if let Some(sqlx_err) = boxed_error.downcast_ref::<SqlxError>() {
        println!("Successfully downcast to SqlxError: {:?}", sqlx_err);
        println!("Error message: {}", sqlx_err);
    } else {
        println!("Failed to downcast to SqlxError");
        println!("Actual error: {}", boxed_error);
    }
    
    Successfully downcast to SqlxError: SqlxError { message: "Database connection failed" }
    Error message: SQLx Error: Database connection failed