How to Fix: Imported functions fail debug assert when returning errors with wasm_backtrace(false)
Wasmtime imported function errors can trip a debug assert when wasm_backtrace(false) is enabled because the runtime still reaches code paths that expect trap metadata for host-call failures.
This issue shows up in a very specific configuration: a host-imported Rust function returns an error, Wasmtime is configured with wasm_backtrace(false), and a debug build hits an internal assertion instead of cleanly propagating the failure. The practical fix is to make sure host errors are surfaced through the right Wasmtime error path, avoid relying on disabled backtrace state during debugging, and use a version of Wasmtime where the mismatch between host error propagation and backtrace expectations has been corrected.
Reproducing the failure
The bug typically appears when an imported host function returns an error such as anyhow::Error through a Result<T>-based callback, while the engine has WebAssembly backtraces disabled:
use anyhow::{anyhow, Result};
use wasmtime::*;
struct MyState;
fn main() -> Result<()> {
let mut config = Config::new();
config.wasm_backtrace(false);
let engine = Engine::new(&config)?;
let mut store = Store::new(&engine, MyState);
let func = Func::wrap(&mut store, || -> Result<(), anyhow::Error> {
Err(anyhow!("host import failed"))
});
let module = Module::new(
&engine,
r#"(module
(import "host" "fail" (func $fail))
(func (export "run")
call $fail)
)"#,
)?;
let instance = Instance::new(&mut store, &module, &[func.into()])?;
let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
let result = run.call(&mut store, ());
println!("result = {:?}", result);
Ok(())
}
In affected builds, this can trigger an internal debug assertion instead of returning a normal Wasmtime error.
Understanding the Root Cause
The core problem is an inconsistency between host function error propagation and optional trap/backtrace bookkeeping.
When a WebAssembly module calls an imported Rust function, Wasmtime bridges execution between compiled Wasm code and host code. If the host function returns an error, Wasmtime must convert that error into its internal failure representation so the call can unwind safely back into the embedding API.
Normally, that failure path may include:
- the original host error payload,
- a trap-like representation used during unwinding, and
- optional Wasm backtrace metadata for diagnostics.
With wasm_backtrace(false), the engine is explicitly told not to collect Wasm backtrace information. In the affected implementation, a debug-only assertion can still assume that certain trap-context or backtrace-related state exists when a host import fails. That assumption is valid for some trap paths, but not for the path where an imported function returns an error while backtraces are disabled.
So the crash is not caused by your business logic, your anyhow usage, or the Wasm module itself. It is caused by an internal invariant that becomes false under this combination:
- imported function returns Err(…),
- error is translated into Wasmtime runtime failure,
- wasm_backtrace collection is disabled,
- debug assertions are enabled.
In release mode, you may only see ordinary error propagation. In debug mode, the assertion makes the latent runtime bug visible.
Step-by-Step Solution
The safest resolution is to combine a Wasmtime upgrade with a small review of how host errors are surfaced.
1. Upgrade to a Wasmtime version containing the fix
If you are on an older release, update first. This issue is fundamentally in runtime behavior, so application-side workarounds are secondary.
[dependencies]
wasmtime = "<latest-compatible-version>"
anyhow = "1"
After upgrading, rebuild your project from scratch:
cargo clean
cargo build
cargo test
If you maintain a lockfile in CI, update it as well:
cargo update -p wasmtime
2. Keep host function errors on the standard Wasmtime error path
Imported functions should return errors using the supported callback signature rather than forcing a panic or mixing incompatible trap strategies.
use anyhow::{anyhow, Result};
use wasmtime::*;
struct MyState;
fn main() -> Result<()> {
let mut config = Config::new();
config.wasm_backtrace(false);
let engine = Engine::new(&config)?;
let mut store = Store::new(&engine, MyState);
let fail = Func::wrap(&mut store, || -> Result<(), anyhow::Error> {
Err(anyhow!("host import failed"))
});
let module = Module::new(
&engine,
r#"(module
(import "host" "fail" (func $fail))
(func (export "run")
call $fail)
)"#,
)?;
let instance = Instance::new(&mut store, &module, &[fail.into()])?;
let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
match run.call(&mut store, ()) {
Ok(()) => println!("completed successfully"),
Err(e) => {
println!("wasm call failed: {e}");
}
}
Ok(())
}
This keeps error handling explicit and avoids masking a runtime bug with a different failure mode.
3. Use wasm_backtrace(true) temporarily if you need an immediate workaround
If upgrading is not immediately possible, enabling backtraces often avoids this exact assertion path because the runtime has the diagnostic state it expects.
let mut config = Config::new();
config.wasm_backtrace(true);
This is a workaround, not the long-term fix. It changes diagnostic behavior and may slightly affect performance.
4. Distinguish host errors from traps in your embedding layer
From an API perspective, both can appear as call failures, but they represent different causes. Keep your error reporting clear so debugging stays easy after the runtime issue is fixed.
match run.call(&mut store, ()) {
Ok(()) => {}
Err(err) => {
eprintln!("guest invocation failed: {err}");
if let Some(trap) = err.downcast_ref::<Trap>() {
eprintln!("trap kind: {trap}");
} else {
eprintln!("non-trap host error propagated through Wasmtime");
}
}
}
This does not fix the assertion by itself, but it prevents confusion between a genuine WebAssembly trap and a host-side callback error.
5. Verify the fix with a regression test
Add a test that specifically exercises the failing path so future dependency changes do not reintroduce it.
#[test]
fn imported_error_with_backtrace_disabled_returns_error_not_assert() -> anyhow::Result<()> {
use anyhow::anyhow;
use wasmtime::*;
let mut config = Config::new();
config.wasm_backtrace(false);
let engine = Engine::new(&config)?;
let mut store = Store::new(&engine, ());
let func = Func::wrap(&mut store, || -> Result<(), anyhow::Error> {
Err(anyhow!("expected host failure"))
});
let module = Module::new(
&engine,
r#"(module
(import "host" "fail" (func $fail))
(func (export "run")
call $fail)
)"#,
)?;
let instance = Instance::new(&mut store, &module, &[func.into()])?;
let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
let err = run.call(&mut store, ()).unwrap_err();
assert!(err.to_string().contains("expected host failure"));
Ok(())
}
Common Edge Cases
- Panic inside the imported function: A Rust panic is not the same as returning Err(…). Depending on configuration and unwind strategy, a panic may abort, be caught differently, or produce a distinct Wasmtime failure path.
- Using older lockfile versions: You may think you upgraded Wasmtime, but Cargo.lock can keep an older transitive version in place. Always verify with cargo tree.
- Confusing host errors with guest traps: A guest unreachable instruction and a host callback error may both fail a call, but they are not operationally equivalent.
- Debug-only visibility: The assertion may appear only in debug builds. Do not assume the issue is harmless just because release builds continue running.
- Backtrace-dependent tests: If your tests assert on exact error text, toggling wasm_backtrace can change formatting and break snapshots.
- Async embeddings: If you are using async host functions or fibers, make sure your Wasmtime version fixes the same error path in the async runtime components too.
FAQ
Why does this happen only when wasm_backtrace(false) is set?
Because the affected runtime path incorrectly assumes that backtrace-related trap state is available even when you explicitly disabled backtrace collection. The assertion exposes that mismatch.
Is returning anyhow::Error from an imported function unsupported?
No. Returning a supported Result-based host error is a normal embedding pattern. The problem is the specific runtime bug in how that error was handled under disabled backtraces.
Can I safely work around this by enabling backtraces permanently?
Usually yes, if the performance and diagnostic tradeoff is acceptable. But the better fix is to upgrade Wasmtime so host function errors work correctly regardless of the wasm_backtrace setting.
In short, this GitHub issue is a Wasmtime runtime invariant bug, not an application misuse bug. The reliable path forward is to upgrade Wasmtime, keep imported function failures on the normal Result-based host error path, and add a regression test that calls a failing host import with wasm_backtrace(false).