neon-bindings/neon · error
Attempted to dereference a `neon::handle::Root` from the…
Error message
Attempted to dereference a `neon::handle::Root` from the wrong module
What it means
`Root` stores an `instance_id` captured when it was created and compares it against the current Neon module instance before dereferencing the underlying `NapiRef`. If the Root is used from a different module instance (e.g. another copy of the .node addon loaded in a worker thread or a second dlopen of the same library), the ids differ and Neon panics to avoid dereferencing a foreign pointer.
Solutions
- Ensure only one copy of the addon is loaded: require it via a consistent specifier/path in all threads and workers.
- Don't send or retain `Root`-containing state across worker/module boundaries; reconstruct the value in the target context instead.
- Use `Root::into_inner` (consuming the Root) before crossing a boundary, then recreate it in the destination context.
- Check for duplicate addon files (e.g. nested node_modules copies) and deduplicate with package-manager hoisting/resolution config.
Example fix
// before let root = cx.root(&obj); worker_tx.send(root); // Root used later in another module instance -> panic // after let value = root.into_inner(cx); // consume in the owning context worker_tx.send(serde_value); // send plain data, rebuild on the other side
Defensive patterns
Strategy: type-guard
Validate before calling
// ensure a single addon instance is loaded everywhere
const addon = require('../native'); // one canonical path, no relative duplicates
// in workers, reuse the same module specifier, never a copied build output Type guard
fn root_belongs_to_this_instance<T>(root: &Root<T>, cx: &FunctionContext) -> bool {
// expose/check via APIs that accept cx: any operation that succeeds on cx
// implies same instance; wrap first use in catch_unwind if hardening
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = root.clone(); // cheap op that still validates instance_id
})).is_ok()
} Try / catch
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
root_clone.into_inner(cx)
})); Prevention
- Load the native module via exactly one path in the whole app, including workers
- Never ship Root values across worker_threads or module boundaries; send data instead
- Consume Roots with into_inner before any context switch
- Watch for duplicate addon copies from bundlers/test runners and dedupe module resolution
When it happens
Trigger: Creating a `Root` in one loaded instance of the native module and passing it to (or keeping it alive into) code running under a different instance — e.g. two `require`s of the same addon resolved to different copies, a `worker_threads` Worker loading its own copy, or the module being unloaded and reloaded while a Root survives.
Common situations: Passing Rust objects holding `Root`s across `worker_threads` boundaries; bundlers/test runners (jest, ts-node with ESM) loading the addon twice under different paths; a shared library loaded both globally and locally so two instances exist.
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
- Must call `into_inner` or `drop` on `neon::handle::Root`
- u32 overflow ocurred in Lifecycle InstanceId
- class must be implemented for a type name
- try_catch: unexpected Err(Throw) when VM is not in a…
- The `neon::main` macro must only be used once
AI-assisted analysis of neon-bindings/neon@38960e4381 (2026-09-13).
Data as JSON: /api/errors/8e229f3f625f9471.
Report an issue: GitHub.
Appendix: source
Thrown at crates/neon/src/handle/root.rs:182
/// Access the inner JavaScript object without consuming the `Root`
/// This method aliases the reference without changing the reference count. It
/// can be used in place of a clone immediately followed by a call to `into_inner`.
///
/// # Panics
///
/// This method panics if it is called from a different JavaScript thread than the
/// one in which the handle was created.
pub fn to_inner<'a, C: Context<'a>>(&self, cx: &mut C) -> Handle<'a, T> {
let env = cx.env();
let local = unsafe { reference::get(env.to_raw(), self.as_napi_ref(cx).0 as *mut _) };
Handle::new_internal(unsafe { T::from_local(env, local) })
}
fn as_napi_ref<'a, C: Context<'a>>(&self, cx: &mut C) -> &NapiRef {
if self.instance_id != instance_id(cx) {
panic!("Attempted to dereference a `neon::handle::Root` from the wrong module ");
}
self.internal
.as_ref()
// `unwrap` will not `panic` because `internal` will always be `Some`
// until the `Root` is consumed.
.unwrap()
}
fn into_napi_ref<'a, C: Context<'a>>(mut self, cx: &mut C) -> NapiRef {
let reference = self.as_napi_ref(cx).clone();
// This uses `as_napi_ref` instead of `Option::take` for the instance id safety check
self.internal = None;
reference
}
}
// Allows putting `Root<T>` directly in a container that implements `Finalize`View on GitHub (pinned to 38960e4381)