neon-bindings/neon · error

Must call `into_inner` or `drop` on `neon::handle::Root`

Error message

Must call `into_inner` or `drop` on `neon::handle::Root`

What it means

`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.

Solutions

  1. Always consume the Root via `root.into_inner(cx)` when you are done with it, inside the same function or an explicit cleanup path.
  2. Store Roots in a container whose Drop consumes them, or use `defer`/finalize hooks to release Roots on the JS thread.
  3. Restructure code so Roots are short-lived: convert to `Handle` promptly and keep only Rust-side data.
  4. 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.

Example fix

// before
struct Cache { root: Root<JsObject> }
impl Drop for Cache { fn drop(&mut self) {} } // Root unconsumed at drop -> panic

// after
impl Cache {
    fn clear(mut self, cx: &mut FunctionContext) {
        let _obj = self.root.into_inner(cx); // consume before dropping
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// before releasing a struct holding a Root, require an explicit consume path
fn validate_cache_clean(cache: &mut Option<Root<JsObject>>) {
    assert!(cache.is_none(), "Root must be into_inner'd before drop");
}

Try / catch

// avoid panics on error paths: settle/consume before `?`
fn step(root: Root<JsObject>, cx: &mut FunctionContext) -> NeonResult<()> {
    let obj = root.into_inner(cx); // always consume first
    work(cx, &obj)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of neon-bindings/neon@38960e4381 (2026-09-13). Data as JSON: /api/errors/81e62470f631d74c. Report an issue: GitHub.

Appendix: source

Thrown at crates/neon/src/handle/root.rs:225

impl<T> Drop for Root<T> {
    #[cfg(not(feature = "napi-6"))]
    fn drop(&mut self) {
        // If `None`, the `NapiRef` has already been manually dropped
        if self.internal.is_none() {
            return;
        }

        // Destructors are called during stack unwinding, prevent a double
        // panic and instead prefer to leak.
        if std::thread::panicking() {
            eprintln!("Warning: neon::handle::Root leaked during a panic");
            return;
        }

        // Only panic if the event loop is still running
        if let Ok(true) = crate::context::internal::IS_RUNNING.try_with(|v| *v.borrow()) {
            panic!("Must call `into_inner` or `drop` on `neon::handle::Root`");
        }
    }

    #[cfg(feature = "napi-6")]
    fn drop(&mut self) {
        // If `None`, the `NapiRef` has already been manually dropped
        if let Some(internal) = self.internal.take() {
            let _ = self.drop_queue.call(DropData::Ref(internal), None);
        }
    }
}

View on GitHub (pinned to 38960e4381)