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

  1. Return ordinary `NeonResult` errors (e.g. `cx.throw_error("...")?`) from the closure instead of manually building `Err(Throw)` values.
  2. 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.
  3. Pin matching `neon` and `neon-runtime`/`sys` crate versions in Cargo.toml and rebuild.
  4. 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

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


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)