neon-bindings/neon · error
Must settle a `neon::types::JsPromise` with…
Error message
Must settle a `neon::types::JsPromise` with `neon::types::Deferred`
What it means
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.
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.
Example fix
// before
let (promise, deferred) = JsPromise::new(cx);
if bad_input {
return cx.throw_error("bad"); // deferred dropped unsettled -> panic
}
// after
let (promise, deferred) = JsPromise::new(cx);
if bad_input {
deferred.reject(cx, "bad input")?; // settle before returning
return Ok(promise);
} Defensive patterns
Strategy: validation
Validate before calling
// track outstanding deferreds; on shutdown, reject any still pending
const pending = new Set();
function track(d) { pending.add(d); return () => pending.delete(d); }
// before event-loop teardown: assert pending.size === 0 Type guard
fn is_settled(d: &Deferred) -> bool { /* napi-6: a settled Deferred stores None internally */ false } // prefer design: always settle in a guard Try / catch
let (promise, deferred) = JsPromise::new(cx);
let result = (|| -> NeonResult<_> {
let v = risky(cx)?;
deferred.resolve(cx, v)?;
Ok(())
})();
if result.is_err() { deferred.reject(cx, "failed")?; } // settle on all paths Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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
- Unexpected `Channel` in sync method. Use `&mut…
- Expected an owned `Channel` instead of a reference.
- Expected an owned `Channel` instead of a context reference.
- Context is not available in async functions. Try a…
- Cannot combine `async fn` with `#[neon(async)]` attribute
AI-assisted analysis of neon-bindings/neon@38960e4381 (2026-09-13).
Data as JSON: /api/errors/dead35e4a8189dcf.
Report an issue: GitHub.
Appendix: source
Thrown at crates/neon/src/types_impl/promise.rs:463
impl Drop for Deferred {
#[cfg(not(feature = "napi-6"))]
fn drop(&mut self) {
// If `None`, the `Deferred` has already been settled
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::types::JsPromise 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 settle a `neon::types::JsPromise` with `neon::types::Deferred`");
}
}
#[cfg(feature = "napi-6")]
fn drop(&mut self) {
// If `None`, the `Deferred` has already been settled
if let Some(internal) = self.internal.take() {
let _ = self.drop_queue.call(DropData::Deferred(internal), None);
}
}
}
#[cfg(all(feature = "napi-5", feature = "futures"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "napi-5", feature = "futures"))))]
/// A type of JavaScript
/// [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
/// object that acts as a [`Future`](std::future::Future).
///View on GitHub (pinned to 38960e4381)