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

  1. Ensure only one copy of the addon is loaded: require it via a consistent specifier/path in all threads and workers.
  2. Don't send or retain `Root`-containing state across worker/module boundaries; reconstruct the value in the target context instead.
  3. Use `Root::into_inner` (consuming the Root) before crossing a boundary, then recreate it in the destination context.
  4. 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

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


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)