{"record":{"id":"dead35e4a8189dcf","repo":"neon-bindings/neon","slug":"must-settle-a-neon-types-jspromise-with-neon-types-deferred","errorCode":null,"errorMessage":"Must settle a `neon::types::JsPromise` with `neon::types::Deferred`","messagePattern":"Must settle a `neon::types::JsPromise` with `neon::types::Deferred`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/neon/src/types_impl/promise.rs","lineNumber":463,"sourceCode":"\nimpl Drop for Deferred {\n    #[cfg(not(feature = \"napi-6\"))]\n    fn drop(&mut self) {\n        // If `None`, the `Deferred` has already been settled\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::types::JsPromise 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 settle a `neon::types::JsPromise` with `neon::types::Deferred`\");\n        }\n    }\n\n    #[cfg(feature = \"napi-6\")]\n    fn drop(&mut self) {\n        // If `None`, the `Deferred` has already been settled\n        if let Some(internal) = self.internal.take() {\n            let _ = self.drop_queue.call(DropData::Deferred(internal), None);\n        }\n    }\n}\n\n#[cfg(all(feature = \"napi-5\", feature = \"futures\"))]\n#[cfg_attr(docsrs, doc(cfg(all(feature = \"napi-5\", feature = \"futures\"))))]\n/// A type of JavaScript\n/// [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n/// object that acts as a [`Future`](std::future::Future).\n///","sourceCodeStart":445,"sourceCodeEnd":481,"githubUrl":"https://github.com/neon-bindings/neon/blob/38960e4381d9ad13b551cdf2d261f609167c9bc2/crates/neon/src/types_impl/promise.rs#L445-L481","documentation":"A `Deferred` created from `JsPromise::new` must eventually call `resolve`/`reject`/`try_catch` to settle its promise. `Deferred::drop` checks `IS_RUNNING` and panics if an unsettled Deferred is dropped while the event loop is still running, because dropping it silently leaks a forever-pending promise. Like Root, it only warns if the thread is already panicking.","triggerScenarios":"Dropping a `Deferred` without calling `resolve`, `reject`, or `try_catch` on it — e.g. discarding the Deferred after `JsPromise::new`, early-returning with `?` before settling, or clearing a map of pending Deferreds while the runtime is alive.","commonSituations":"Async task registries that evict entries without rejecting; error paths in async functions that return before settling; refactors that move the settle calls into a code path never reached; timeout logic that forgets to reject the promise.","solutions":["Guarantee every Deferred is settled exactly once: call `deferred.resolve(cx, value)` or `deferred.reject(cx, err)` on all paths, including error paths.","Store the Deferred with its promise and settle it in a completion/finalize callback rather than dropping it.","On abort/timeout/cancel paths, call `reject` with a cancellation error instead of dropping the Deferred.","Structure settle logic with a guard (e.g. settle inside a scope that runs even on early return) so `?`/panics can't bypass it."],"exampleFix":"// before\nlet (promise, deferred) = JsPromise::new(cx);\nif bad_input {\n    return cx.throw_error(\"bad\"); // deferred dropped unsettled -> panic\n}\n\n// after\nlet (promise, deferred) = JsPromise::new(cx);\nif bad_input {\n    deferred.reject(cx, \"bad input\")?; // settle before returning\n    return Ok(promise);\n}","handlingStrategy":"validation","validationCode":"// track outstanding deferreds; on shutdown, reject any still pending\nconst pending = new Set();\nfunction track(d) { pending.add(d); return () => pending.delete(d); }\n// before event-loop teardown: assert pending.size === 0","typeGuard":"fn is_settled(d: &Deferred) -> bool { /* napi-6: a settled Deferred stores None internally */ false } // prefer design: always settle in a guard","tryCatchPattern":"let (promise, deferred) = JsPromise::new(cx);\nlet result = (|| -> NeonResult<_> {\n    let v = risky(cx)?;\n    deferred.resolve(cx, v)?;\n    Ok(())\n})();\nif result.is_err() { deferred.reject(cx, \"failed\")?; } // settle on all paths","preventionTips":["Settle every Deferred (resolve/reject) exactly once, on success AND error paths","Reject on cancellation/timeout instead of dropping the Deferred","Keep the Deferred with its owning async operation so its lifetime is visible","In registries, evict by rejecting, never by removing silently"],"tags":["rust","neon","promises","async"],"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"}