{"record":{"id":"81e62470f631d74c","repo":"neon-bindings/neon","slug":"must-call-into-inner-or-drop-on-neon-handle-root","errorCode":null,"errorMessage":"Must call `into_inner` or `drop` on `neon::handle::Root`","messagePattern":"Must call `into_inner` or `drop` on `neon::handle::Root`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/neon/src/handle/root.rs","lineNumber":225,"sourceCode":"\nimpl<T> Drop for Root<T> {\n    #[cfg(not(feature = \"napi-6\"))]\n    fn drop(&mut self) {\n        // If `None`, the `NapiRef` has already been manually dropped\n        if self.internal.is_none() {\n            return;\n        }\n\n        // Destructors are called during stack unwinding, prevent a double\n        // panic and instead prefer to leak.\n        if std::thread::panicking() {\n            eprintln!(\"Warning: neon::handle::Root leaked during a panic\");\n            return;\n        }\n\n        // Only panic if the event loop is still running\n        if let Ok(true) = crate::context::internal::IS_RUNNING.try_with(|v| *v.borrow()) {\n            panic!(\"Must call `into_inner` or `drop` on `neon::handle::Root`\");\n        }\n    }\n\n    #[cfg(feature = \"napi-6\")]\n    fn drop(&mut self) {\n        // If `None`, the `NapiRef` has already been manually dropped\n        if let Some(internal) = self.internal.take() {\n            let _ = self.drop_queue.call(DropData::Ref(internal), None);\n        }\n    }\n}\n","sourceCodeStart":207,"sourceCodeEnd":237,"githubUrl":"https://github.com/neon-bindings/neon/blob/38960e4381d9ad13b551cdf2d261f609167c9bc2/crates/neon/src/handle/root.rs#L207-L237","documentation":"`Root::drop` checks whether the Root still holds an un-consumed reference when it is dropped. If the event loop is still running (`IS_RUNNING`), dropping a Root without calling `into_inner` means the underlying JS value reference leaks, so Neon panics to surface the bug. If the thread is panicking already, it only prints a warning to avoid double-panics.","triggerScenarios":"Letting a `Root` value go out of scope (or be discarded) without calling `into_inner`, in code running while the Node event loop is alive — e.g. overwriting a Root field with a new Root, returning early with `?` while a local Root is alive and unconsumed, or storing Roots in a struct that is dropped at runtime.","commonSituations":"Caching JS objects in long-lived Rust structs and forgetting to consume the Root during cleanup; error paths that skip `into_inner`; holding Roots in `HashMap` entries that get `remove`d/`clear`ed while the server runs.","solutions":["Always consume the Root via `root.into_inner(cx)` when you are done with it, inside the same function or an explicit cleanup path.","Store Roots in a container whose Drop consumes them, or use `defer`/finalize hooks to release Roots on the JS thread.","Restructure code so Roots are short-lived: convert to `Handle` promptly and keep only Rust-side data.","If a Root must live for the program's lifetime, intentionally leak it (e.g. `std::mem::forget` / `Box::leak`) as a documented choice so Drop never sees it un-consumed."],"exampleFix":"// before\nstruct Cache { root: Root<JsObject> }\nimpl Drop for Cache { fn drop(&mut self) {} } // Root unconsumed at drop -> panic\n\n// after\nimpl Cache {\n    fn clear(mut self, cx: &mut FunctionContext) {\n        let _obj = self.root.into_inner(cx); // consume before dropping\n    }\n}","handlingStrategy":"validation","validationCode":"// before releasing a struct holding a Root, require an explicit consume path\nfn validate_cache_clean(cache: &mut Option<Root<JsObject>>) {\n    assert!(cache.is_none(), \"Root must be into_inner'd before drop\");\n}","typeGuard":null,"tryCatchPattern":"// avoid panics on error paths: settle/consume before `?`\nfn step(root: Root<JsObject>, cx: &mut FunctionContext) -> NeonResult<()> {\n    let obj = root.into_inner(cx); // always consume first\n    work(cx, &obj)\n}","preventionTips":["Call into_inner on every path, including early returns — consume before using `?`","Model consumed-ness in types: Option<Root> set to None after into_inner","Never overwrite a Root field without consuming the old value","Use finalize/defer hooks to release long-lived Roots on the JS thread"],"tags":["rust","neon","memory-leak","lifecycle"],"backgroundTag":"invalid-state-transition","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"}