{"record":{"id":"478e7b18f1c73eb1","repo":"napi-rs/napi-rs","slug":"promise-finally-callback-was-called-more-than-once","errorCode":null,"errorMessage":"Promise finally callback was called more than once","messagePattern":"Promise finally callback was called more than once","errorType":"error_code","errorClass":"GenericFailure","httpStatus":null,"severity":"error","filePath":"crates/napi/src/bindgen_runtime/js_values/promise_raw.rs","lineNumber":548,"sourceCode":"  check_status!(\n    unsafe {\n      sys::napi_get_cb_info(\n        env,\n        cbinfo,\n        &mut 0,\n        ptr::null_mut(),\n        ptr::null_mut(),\n        &mut rust_cb,\n      )\n    },\n    \"Get callback info from finally callback failed\"\n  )?;\n  // The box is owned by the `napi_wrap` finalizer; only borrow it here and\n  // `take()` the FnOnce out, so a thenable invoking this callback more than\n  // once gets an error instead of a use-after-free.\n  let cb = unsafe { &mut *rust_cb.cast::<Option<Cb>>() };\n  let Some(cb) = cb.take() else {\n    return Err(Error::new(\n      Status::GenericFailure,\n      \"Promise finally callback was called more than once\".to_owned(),\n    ));\n  };\n\n  unsafe { U::to_napi_value(env, cb(Env(env))?) }\n}\n\npub struct CallbackContext<T> {\n  pub env: Env,\n  pub value: T,\n}\n\nimpl<T: ToNapiValue> ToNapiValue for CallbackContext<T> {\n  unsafe fn to_napi_value(env: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value> {\n    T::to_napi_value(env, val.value)\n  }\n}","sourceCodeStart":530,"sourceCodeEnd":566,"githubUrl":"https://github.com/napi-rs/napi-rs/blob/39bd1205e480a453a2da2601a760bde5a71ed016/crates/napi/src/bindgen_runtime/js_values/promise_raw.rs#L530-L566","documentation":"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.","triggerScenarios":"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.","commonSituations":"Custom promise implementations without proper settlement guards; thenables that re-run their handlers when re-emitting events; polyfills predating correct finally semantics (ES2018).","solutions":["Ensure the finally handler runs at most once with a settled flag","Replace custom thenables/polyfills with native Promises or a spec-compliant library","Treat the native finally callback as strictly one-shot; do not attempt to reuse it"],"exampleFix":"// before\nfinallyHandler(); finallyHandler(); // double invocation\n// after\nlet ran = false;\nconst runFinally = () => { if (!ran) { ran = true; finallyHandler(); } };\npromise.then(onOk, onErr).then(runFinally, runFinally);","handlingStrategy":"validation","validationCode":"// JS: single-shot finally\nlet ran = false;\nconst fin = () => { if (!ran) { ran = true; cleanup(); } };","typeGuard":null,"tryCatchPattern":"try { await p } finally { cleanupOnce(); }","preventionTips":["Use ES2018+ native Promise.prototype.finally","Wrap cleanup in a once-guard when composing handlers","Avoid custom polyfills lacking settlement guards"],"tags":["napi","promise","finally","use-after-free-guard"],"backgroundTag":"invalid-state-transition","analyzedSha":"39bd1205e480a453a2da2601a760bde5a71ed016","analyzedAt":"2026-09-13T20:31:47.814Z","contentChangedAt":"2026-09-13T20:31:47.814Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}