napi-rs/napi-rs · error · GenericFailure

Promise finally callback was called more than once

Error message

Promise finally callback was called more than once

What it means

Same single-shot guard applied to the `finally` callback of raw native promises: the stored `Option<Cb>` FnOnce is taken on the first finally invocation, and a repeat invocation returns this GenericFailure rather than touching freed memory owned by the `napi_wrap` finalizer.

Solutions

  1. Ensure the finally handler runs at most once with a settled flag
  2. Replace custom thenables/polyfills with native Promises or a spec-compliant library
  3. Treat the native finally callback as strictly one-shot; do not attempt to reuse it

Example fix

// before
finallyHandler(); finallyHandler(); // double invocation
// after
let ran = false;
const runFinally = () => { if (!ran) { ran = true; finallyHandler(); } };
promise.then(onOk, onErr).then(runFinally, runFinally);
Defensive patterns

Strategy: validation

Validate before calling

// JS: single-shot finally
let ran = false;
const fin = () => { if (!ran) { ran = true; cleanup(); } };

Try / catch

try { await p } finally { cleanupOnce(); }

Prevention

When it happens

Trigger: A thenable or promise polyfill that invokes the finally handler more than once (e.g. a `finally` implementation that calls the callback both for fulfillment and rejection, or re-invokes it on re-settlement) attached to a native raw promise.

Common situations: Custom promise implementations without proper settlement guards; thenables that re-run their handlers when re-emitting events; polyfills predating correct finally semantics (ES2018).

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 napi-rs/napi-rs@39bd1205e4 (2026-09-13). Data as JSON: /api/errors/478e7b18f1c73eb1. Report an issue: GitHub.

Appendix: source

Thrown at crates/napi/src/bindgen_runtime/js_values/promise_raw.rs:548

  check_status!(
    unsafe {
      sys::napi_get_cb_info(
        env,
        cbinfo,
        &mut 0,
        ptr::null_mut(),
        ptr::null_mut(),
        &mut rust_cb,
      )
    },
    "Get callback info from finally callback failed"
  )?;
  // The box is owned by the `napi_wrap` finalizer; only borrow it here and
  // `take()` the FnOnce out, so a thenable invoking this callback more than
  // once gets an error instead of a use-after-free.
  let cb = unsafe { &mut *rust_cb.cast::<Option<Cb>>() };
  let Some(cb) = cb.take() else {
    return Err(Error::new(
      Status::GenericFailure,
      "Promise finally callback was called more than once".to_owned(),
    ));
  };

  unsafe { U::to_napi_value(env, cb(Env(env))?) }
}

pub struct CallbackContext<T> {
  pub env: Env,
  pub value: T,
}

impl<T: ToNapiValue> ToNapiValue for CallbackContext<T> {
  unsafe fn to_napi_value(env: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value> {
    T::to_napi_value(env, val.value)
  }
}

View on GitHub (pinned to 39bd1205e4)