{"record":{"id":"8e229f3f625f9471","repo":"neon-bindings/neon","slug":"attempted-to-dereference-a-neon-handle-root-from-the-wrong","errorCode":null,"errorMessage":"Attempted to dereference a `neon::handle::Root` from the wrong module ","messagePattern":"Attempted to dereference a `neon::handle::Root` from the wrong module ","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/neon/src/handle/root.rs","lineNumber":182,"sourceCode":"\n    /// Access the inner JavaScript object without consuming the `Root`\n    /// This method aliases the reference without changing the reference count. It\n    /// can be used in place of a clone immediately followed by a call to `into_inner`.\n    ///\n    /// # Panics\n    ///\n    /// This method panics if it is called from a different JavaScript thread than the\n    /// one in which the handle was created.\n    pub fn to_inner<'a, C: Context<'a>>(&self, cx: &mut C) -> Handle<'a, T> {\n        let env = cx.env();\n        let local = unsafe { reference::get(env.to_raw(), self.as_napi_ref(cx).0 as *mut _) };\n\n        Handle::new_internal(unsafe { T::from_local(env, local) })\n    }\n\n    fn as_napi_ref<'a, C: Context<'a>>(&self, cx: &mut C) -> &NapiRef {\n        if self.instance_id != instance_id(cx) {\n            panic!(\"Attempted to dereference a `neon::handle::Root` from the wrong module \");\n        }\n\n        self.internal\n            .as_ref()\n            // `unwrap` will not `panic` because `internal` will always be `Some`\n            // until the `Root` is consumed.\n            .unwrap()\n    }\n\n    fn into_napi_ref<'a, C: Context<'a>>(mut self, cx: &mut C) -> NapiRef {\n        let reference = self.as_napi_ref(cx).clone();\n        // This uses `as_napi_ref` instead of `Option::take` for the instance id safety check\n        self.internal = None;\n        reference\n    }\n}\n\n// Allows putting `Root<T>` directly in a container that implements `Finalize`","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/neon-bindings/neon/blob/38960e4381d9ad13b551cdf2d261f609167c9bc2/crates/neon/src/handle/root.rs#L164-L200","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet root = cx.root(&obj);\nworker_tx.send(root); // Root used later in another module instance -> panic\n\n// after\nlet value = root.into_inner(cx); // consume in the owning context\nworker_tx.send(serde_value); // send plain data, rebuild on the other side","handlingStrategy":"type-guard","validationCode":"// ensure a single addon instance is loaded everywhere\nconst addon = require('../native'); // one canonical path, no relative duplicates\n// in workers, reuse the same module specifier, never a copied build output","typeGuard":"fn root_belongs_to_this_instance<T>(root: &Root<T>, cx: &FunctionContext) -> bool {\n    // expose/check via APIs that accept cx: any operation that succeeds on cx\n    // implies same instance; wrap first use in catch_unwind if hardening\n    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n        let _ = root.clone(); // cheap op that still validates instance_id\n    })).is_ok()\n}","tryCatchPattern":"let result = std::panic::catch_unwind(AssertUnwindSafe(|| {\n    root_clone.into_inner(cx)\n}));","preventionTips":["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"],"tags":["rust","neon","workers","lifecycle"],"backgroundTag":"internal-invariant-violation","analyzedSha":"38960e4381d9ad13b551cdf2d261f609167c9bc2","analyzedAt":"2026-09-13T09:05:33.640Z","contentChangedAt":"2026-09-13T09:05:33.640Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}