Skip to content

Fix using downcast with anyhow::Error#12689

Merged
alexcrichton merged 2 commits intobytecodealliance:mainfrom
alexcrichton:fix-anyhow-downcast
Feb 27, 2026
Merged

Fix using downcast with anyhow::Error#12689
alexcrichton merged 2 commits intobytecodealliance:mainfrom
alexcrichton:fix-anyhow-downcast

Conversation

@alexcrichton
Copy link
Member

Previously when converting wasmtime::Error into anyhow::Error it ended up breaking the downcast method. This is because anyhow::Error::from_boxed looks like it does not implement the downcast method which is how all errors were previously converted to anyhow::Error. This commit adds a new vtable method for specifically converting to anyhow::Error which enables using the typed construction methods of anyhow which preserves downcast-ness.

Previously when converting `wasmtime::Error` into `anyhow::Error` it
ended up breaking the `downcast` method. This is because
`anyhow::Error::from_boxed` looks like it does not implement the
`downcast` method which is how all errors were previously converted to
`anyhow::Error`. This commit adds a new vtable method for specifically
converting to `anyhow::Error` which enables using the typed construction
methods of `anyhow` which preserves `downcast`-ness.
@alexcrichton alexcrichton requested a review from fitzgen February 27, 2026 17:35
@alexcrichton alexcrichton requested a review from a team as a code owner February 27, 2026 17:35
Comment on lines -545 to +549
anyhow::Error::from_boxed(e.into_boxed_dyn_error())
e.inner.into_anyhow()
Copy link
Member

@fitzgen fitzgen Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I understand, it isn't enough to change this impl into something like

e.downcast::<anyhow::Error>().unwrap_or_else(|e| {
    anyhow::Error::from_boxed(e.into_boxed_dyn_error())
})

and expose the underlying anyhow::Error (if any) instead of adding another dyn-boxing indirection because anyhow itself doesn't implement is/downcast/etc... for anyhow::Errors created via anyhow::Error::from_boxed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific case that I ran into updating Spin was that a Trap in Wasmtime was converted to wasmtime::Error and then that was converted to anyhow::Error. Calling anyhow_error.downcast::<Trap>() was failing due to being created from anyhow::Error::from_boxed which ends up severing any downcast methods.

So I believe what you've gisted will solve anyhow-and-back but wouldn't solve acquiring Wasmtime's error types which never went through anyhow

Comment on lines +307 to +313
#[cfg(feature = "anyhow")]
fn ext_into_anyhow(mut self) -> anyhow::Error {
match self.error.take() {
Some(error) => anyhow::Error::from(error).context(self.context),
None => anyhow::Error::msg(self),
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess anyhow's is/downcast could never see through our context chains when all they have is a Box<dyn core::error::Error>. Okay, I get it.

Comment on lines +911 to +927
fn anyhow_preserves_downcast() -> Result<()> {
{
let e: Error = TestError(1).into();
assert!(e.downcast_ref::<TestError>().is_some());
let e: anyhow::Error = e.into();
assert!(e.downcast_ref::<TestError>().is_some());
}
{
let e = Error::from(TestError(1)).context("hi");
assert!(e.downcast_ref::<TestError>().is_some());
assert!(e.downcast_ref::<&str>().is_some());
let e: anyhow::Error = e.into();
assert!(e.downcast_ref::<TestError>().is_some());
assert!(e.downcast_ref::<&str>().is_some());
}
Ok(())
}
Copy link
Member

@fitzgen fitzgen Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me realize wasmtime::Error probably doesn't handle is/downcast* correctly when created from an anyhow::Error that has a context chain:

let e = anyhow::Error::from(TestError(1));
let e = e.context("str");
let e = wasmtime::Error::from(e);
assert!(e.is::<anyhow::Error>()); // should pass right now
assert!(e.is::<TestError>());     // I think will fail
assert!(e.is::<&str>());          // I think will also fail

And in fact, I don't think we can support this, given that our vtable methods can't be generic over E but instead must take TypeIds. But maybe I am missing something and you have other ideas?

I guess we could at the wasmtime::Error implementation level fall back to something like

impl wasmtime::Error {
    pub fn is<E>(&self) -> bool {
        let result = existing_is_impl();
        
        #[cfg(feature = "anyhow")]
        let result = result || self.downcast_ref::<anyhow::Error>().is_some_and(|e| {
            e.is::<E>()
        });

        result
    }
}

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, and yeah I think hooking is and downcast and friends with type parameters to explicitly handle the anyhow special case is the only way to go here. I'll see if I can't hook that up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, and yeah I think hooking is and downcast and friends with type parameters to explicitly handle the anyhow special case is the only way to go here. I'll see if I can't hook that up.

Fine to do as a follow up too, ofc, if you prefer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ok yeah this is going to be a little gnarly, so I've added a test for the current behavior documenting that it's a bit buggy, but it at least preserves the anyhow roundtrip. I'll file a follow-up issue for this.

@alexcrichton alexcrichton added this pull request to the merge queue Feb 27, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 27, 2026
@alexcrichton alexcrichton added this pull request to the merge queue Feb 27, 2026
Merged via the queue into bytecodealliance:main with commit 4fd25c0 Feb 27, 2026
45 checks passed
@alexcrichton alexcrichton deleted the fix-anyhow-downcast branch February 27, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants