neon-bindings/neon · error
try_catch: unexpected Err(Throw) when VM is not in a…
Error message
try_catch: unexpected Err(Throw) when VM is not in a throwing state
What it means
`Context::try_catch` runs a closure and distinguishes a JS exception (Err(Throw)) from a Rust error. This panic fires when the closure returned Err(Throw) but the Node-API runtime reports no pending exception (`catch_error` found nothing). Neon treats that combination as an unreachable internal state, so it panics rather than silently swallowing the result.
Solutions
- Return ordinary `NeonResult` errors (e.g. `cx.throw_error("...")?`) from the closure instead of manually building `Err(Throw)` values.
- Remove any raw Node-API calls (e.g. `napi_get_and_clear_last_exception`) inside the closure that could clear a pending exception before try_catch inspects it.
- Pin matching `neon` and `neon-runtime`/`sys` crate versions in Cargo.toml and rebuild.
- If this reproduces on plain Neon code, file a Neon issue with a minimal reproducer — it is a library invariant violation.
Example fix
// before
fn f(cx: &mut FunctionContext) -> JsResult<JsUndefined> {
cx.try_catch(|cx| {
// manually returned Throw without a real pending exception
Err(cx.throw_error("oops").unwrap_err())
})
}
// after
fn f(cx: &mut FunctionContext) -> JsResult<JsUndefined> {
cx.try_catch(|cx| {
cx.throw_error("oops")?; // real pending exception is set
Ok(cx.undefined())
})
} Defensive patterns
Strategy: try-catch
Try / catch
cx.try_catch(|cx| {
// only propagate errors via cx.throw_* / NeonResult
let v = risky(cx)?;
Ok(v)
}).or_else(|err| {
// handle the JS exception here; never fabricate Err(Throw) manually
fallback(cx)
}) Prevention
- Propagate errors through NeonResult and cx.throw_error, never hand-built Throw values
- Don't call raw Node-API exception-clearing functions inside try_catch closures
- Keep neon and its sys/runtime crates on matching versions
- Reproduce weird try_catch panics in a minimal Node script before debugging app code
When it happens
Trigger: A closure passed to `try_catch` returns `Err(Handle<JsValue>::Throw)` while no exception is actually pending on the isolate — typically caused by custom `sys`-level code that clears or never sets a pending exception, or by manual construction of a `Throw` result outside normal error propagation.
Common situations: Writing low-level/native extensions that mix raw Node-API calls with Neon's try_catch; custom `Finalize` or exception-clearing code; running under an alternative runtime (e.g. Deno/older Node) whose error-catching semantics differ; Neon/sys version mismatches.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- class must be implemented for a type name
- The `neon::main` macro must only be used once
- Attempted to dereference a `neon::handle::Root` from the…
- Must call `into_inner` or `drop` on `neon::handle::Root`
- u32 overflow ocurred in Lifecycle InstanceId
AI-assisted analysis of neon-bindings/neon@38960e4381 (2026-09-13).
Data as JSON: /api/errors/8eba03341d88b962.
Report an issue: GitHub.
Appendix: source
Thrown at crates/neon/src/context/internal.rs:44
impl Env {
pub(crate) fn to_raw(self) -> raw::Env {
let Self(ptr) = self;
ptr
}
pub(super) unsafe fn try_catch<T, F>(self, f: F) -> Result<T, raw::Local>
where
F: FnOnce() -> Result<T, crate::result::Throw>,
{
let result = f();
let mut local: MaybeUninit<raw::Local> = MaybeUninit::zeroed();
if sys::error::catch_error(self.to_raw(), local.as_mut_ptr()) {
Err(local.assume_init())
} else if let Ok(result) = result {
Ok(result)
} else {
panic!("try_catch: unexpected Err(Throw) when VM is not in a throwing state");
}
}
}
pub trait ContextInternal<'cx>: Sized {
fn cx(&self) -> &Cx<'cx>;
fn cx_mut(&mut self) -> &mut Cx<'cx>;
fn env(&self) -> Env {
self.cx().env
}
}
fn default_main(mut cx: ModuleContext) -> NeonResult<()> {
#[cfg(all(feature = "napi-6", feature = "tokio-rt-multi-thread"))]
crate::executor::tokio::init(&mut cx)?;
crate::registered().export(&mut cx)
}
View on GitHub (pinned to 38960e4381)