Skip to content

Comments

fix(napi): memory leak in async fn#3089

Merged
Brooooooklyn merged 1 commit intomainfrom
01-08-fix_napi_memory_leak_in_async_fn
Jan 8, 2026
Merged

fix(napi): memory leak in async fn#3089
Brooooooklyn merged 1 commit intomainfrom
01-08-fix_napi_memory_leak_in_async_fn

Conversation

@Brooooooklyn
Copy link
Member

@Brooooooklyn Brooooooklyn commented Jan 8, 2026


Note

Addresses async memory leaks by ensuring N-API references are released when async promises settle.

  • Core: Introduces execute_tokio_future_with_finalize_callback, JsDeferred::set_finalize_callback, and finalize-callback plumbing in deferred.rs to run cleanup on resolve/reject; fixes TSFN release handling
  • Codegen: Replaces execute_tokio_future usage with execute_tokio_future_with_finalize_callback and passes _args_ref.drop(env) as the finalize callback
  • Examples: Adds AsyncThrowClass with asyncThrowError, updates exports and type defs
  • Tests/CI: Adds memory-test.ts and test:leak script (via @oxc-node/cli) and runs it in CI on Linux/macOS/Windows
  • Misc: Updates snapshots and lockfile to include new example and dependencies

Written by Cursor Bugbot for commit 4601780. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Added AsyncThrowClass with a constructor and asyncThrowError() exported in examples.
    • Added optional finalize-callback support to the async runtime to ensure cleanup after async tasks.
  • Tests

    • Added a memory-leak test that exercises AsyncThrowClass and integrated it into CI test runs.
  • Chores

    • Added a leak-test script and a dev dependency to support GC-based leak testing.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 8, 2026 10:38
@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Adds an optional finalize-callback path for async N-API futures: the callback is threaded through JsDeferred and the tokio executor, codegen is updated to use the new executor, and example/tests + CI leak-test wiring are added to validate finalization.

Changes

Cohort / File(s) Summary
Runtime core
crates/napi/src/js_values/deferred.rs, crates/napi/src/tokio_runtime.rs, crates/backend/src/codegen/fn.rs
Add finalize-callback support: JsDeferred holds an optional finalize callback with setter; new execute_tokio_future_with_finalize_callback accepts and wires finalize callbacks; codegen calls the new executor and moves argument/reference drop to the finalize callback.
Examples & API surface
examples/napi/src/async.rs, examples/napi/index.d.cts, examples/napi/index.cjs, examples/napi/example.wasi.cjs, examples/napi/example.wasi-browser.js, examples/napi/__tests__/__snapshots__/values.spec.ts.md
Introduce AsyncThrowClass (constructor + asyncThrowError/async_throw_error) and expose it in JS/CTS/WASI wrappers and snapshots.
Memory test & package
examples/napi/memory-test.ts, examples/napi/package.json
Add memory-leak test script test:leak (uses @oxc-node/cli) and a GC-based example test that exercises AsyncThrowClass.
CI
.github/workflows/test-release.yaml
Add yarn workspace @examples/napi test:leak invocation in three matrix jobs to run leak tests in CI.

Sequence Diagram(s)

sequenceDiagram
    participant Codegen as Generated Callback (fn.rs)
    participant Tokio as Tokio Runtime (tokio_runtime.rs)
    participant Deferred as JsDeferred (deferred.rs)
    participant NAPI as N-API Env

    Codegen->>Tokio: call execute_tokio_future_with_finalize_callback(fut, resolver, finalize_cb)
    Tokio->>Deferred: create JsDeferred (includes finalize_cb) and tsfn
    Tokio->>Tokio: spawn async task
    Note over Tokio: async task completes -> Result<Data, Error>
    Tokio->>Deferred: send result via tsfn / resolver wrapper
    Deferred->>NAPI: napi_resolve_deferred / napi_reject_deferred (invoke resolver)
    Deferred->>NAPI: invoke finalize_cb(env)  -- perform cleanup (drop refs)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through futures, left a tidy track,

Passed a finalize note to bring old refs back,
When async calls finish, I nibble the thread,
No stray crumbs left — the heap sleeps in bed,
Cheers to leak tests and a neat little nap.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(napi): memory leak in async fn' directly summarizes the main change—addressing memory leaks in async functions, which aligns with the core objective of the PR.
Linked Issues check ✅ Passed The PR implements all key requirements: adds finalize callback mechanism to JsDeferred [#3086, #1903], updates code generation to use execute_tokio_future_with_finalize_callback [#3086, #1903], adds AsyncThrowClass example with memory-test.ts [#3086, #1903], and includes CI leak detection steps [#3086, #1903].
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the async memory leak: finalize callback infrastructure, code generation updates, example test cases, and CI leak tests. The test workflow additions and dependency updates support leak detection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member Author


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • ready-to-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6b11285ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/napi/src/js_values/deferred.rs (1)

298-321: Missing finalize_callback invocation in error path.

When error is Err(err) (lines 305-316), the finalize_callback is not invoked. This could cause a resource leak if deferred_data.trace.into_rejected fails.

🐛 Proposed fix
       Err(err) => {
         if cfg!(debug_assertions) {
           eprintln!("Failed to reject deferred: {err:?}");
           let mut err = ptr::null_mut();
           let mut err_msg = ptr::null_mut();
           unsafe {
             sys::napi_create_string_utf8(env, c"Rejection failed".as_ptr().cast(), 0, &mut err_msg);
             sys::napi_create_error(env, ptr::null_mut(), err_msg, &mut err);
             sys::napi_reject_deferred(env, deferred, err);
           }
         }
+        if let Some(finalize_callback) = finalize_callback {
+          finalize_callback(env);
+        }
       }
     }
   } else {
🤖 Fix all issues with AI agents
In @examples/napi/package.json:
- Line 34: The package.json dependency for "@oxc-node/cli" is pinned to a
non-existent version "^0.0.35"; change the version string for the
"@oxc-node/cli" entry from "^0.0.35" to a valid published version (e.g.
"^0.0.32" or "0.0.32") and then run npm install to verify the dependency
resolves successfully.
🧹 Nitpick comments (3)
examples/napi/memory-test.ts (1)

13-18: Consider adding a timeout to fail the test if the object is not collected.

The test runs indefinitely if the WeakRef never dereferences to undefined. Consider adding a maximum iteration count or timeout to exit with a non-zero code if the leak persists.

♻️ Suggested improvement
+let iterations = 0
+const MAX_ITERATIONS = 30 // 30 seconds timeout
+
 setInterval(() => {
   global.gc?.()
+  iterations++
   if (wr.deref() === undefined) {
     console.info('No leak')
     process.exit(0)
   }
+  if (iterations >= MAX_ITERATIONS) {
+    console.error('Memory leak detected: object was not garbage collected')
+    process.exit(1)
+  }
 }, 1000)
crates/napi/src/tokio_runtime.rs (1)

253-320: Consider reducing code duplication with execute_tokio_future.

The new function is nearly identical to execute_tokio_future. Consider refactoring to have execute_tokio_future call execute_tokio_future_with_finalize_callback with None to reduce duplication.

♻️ Suggested refactor
 #[cfg(not(feature = "noop"))]
 #[allow(clippy::not_unsafe_ptr_arg_deref)]
 pub fn execute_tokio_future<
   Data: 'static + Send,
   Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
   Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
 >(
   env: sys::napi_env,
   fut: Fut,
   resolver: Resolver,
 ) -> Result<sys::napi_value> {
-  let env = Env::from_raw(env);
-  let (deferred, promise) = JsDeferred::new(&env)?;
-  // ... rest of the duplicated code
+  execute_tokio_future_with_finalize_callback(env, fut, resolver, None)
 }
crates/napi/src/js_values/deferred.rs (1)

170-175: Consider simplifying finalize_callback handling.

The set_finalize_callback method creates a new Arc<RwLock<...>> wrapper each time, which means the previous Arc is orphaned. Since this is only called once before use, this is functionally correct but could be simplified.

♻️ Alternative approach

Instead of creating a new Arc, you could write to the existing RwLock:

   pub fn set_finalize_callback(
     &mut self,
     finalize_callback: Option<Box<dyn FnOnce(sys::napi_env)>>,
   ) {
-    self.finalize_callback = Arc::new(RwLock::new(finalize_callback));
+    *self.finalize_callback.write().expect("RwLock Poison") = finalize_callback;
   }

This avoids creating a new Arc allocation, though in practice the impact is minimal.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa4ffb2 and c6b1128.

⛔ Files ignored due to path filters (2)
  • examples/napi/__tests__/__snapshots__/values.spec.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/test-release.yaml
  • crates/backend/src/codegen/fn.rs
  • crates/napi/src/js_values/deferred.rs
  • crates/napi/src/tokio_runtime.rs
  • examples/napi/__tests__/__snapshots__/values.spec.ts.md
  • examples/napi/example.wasi-browser.js
  • examples/napi/example.wasi.cjs
  • examples/napi/index.cjs
  • examples/napi/index.d.cts
  • examples/napi/memory-test.ts
  • examples/napi/package.json
  • examples/napi/src/async.rs
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi]` attribute on impl blocks to define classes exported to JavaScript/TypeScript

Applied to files:

  • examples/napi/example.wasi.cjs
  • examples/napi/src/async.rs
  • examples/napi/index.cjs
  • crates/napi/src/js_values/deferred.rs
  • examples/napi/example.wasi-browser.js
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `yarn workspace examples/napi build` before testing to ensure code generation reflects current Rust changes

Applied to files:

  • .github/workflows/test-release.yaml
  • examples/napi/package.json
  • examples/napi/src/async.rs
  • examples/napi/memory-test.ts
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `cargo test` for Rust unit tests and `yarn workspace examples/napi test` for integration tests

Applied to files:

  • .github/workflows/test-release.yaml
  • examples/napi/package.json
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Rebuild TypeScript definitions after making changes to Rust code using `yarn workspace examples/napi build`

Applied to files:

  • .github/workflows/test-release.yaml
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi(js_name = "...")]` attribute to rename Rust items in JavaScript/TypeScript

Applied to files:

  • examples/napi/src/async.rs
  • crates/napi/src/js_values/deferred.rs
🧬 Code graph analysis (5)
examples/napi/index.d.cts (1)
examples/napi/example.wasi-browser.js (2)
  • AsyncThrowClass (77-77)
  • AsyncThrowClass (77-77)
examples/napi/src/async.rs (5)
examples/napi/example.wasi-browser.js (4)
  • AsyncThrowClass (77-77)
  • AsyncThrowClass (77-77)
  • Status (357-357)
  • Status (357-357)
crates/napi/src/tokio_runtime.rs (1)
  • new (335-340)
crates/napi/src/bindgen_runtime/callback_info.rs (1)
  • new (27-80)
crates/napi/src/bindgen_runtime/js_values/array.rs (1)
  • new (14-29)
crates/napi/src/threadsafe_function.rs (1)
  • new (53-59)
crates/backend/src/codegen/fn.rs (1)
crates/napi/src/tokio_runtime.rs (2)
  • execute_tokio_future_with_finalize_callback (256-320)
  • new (335-340)
crates/napi/src/tokio_runtime.rs (6)
crates/napi/src/bindgen_runtime/js_values/object.rs (2)
  • finalize_callback (1030-1053)
  • from_raw (704-713)
crates/napi/src/js_values/object.rs (1)
  • finalize_callback (91-112)
crates/napi/src/env.rs (1)
  • from_raw (72-74)
crates/napi/src/js_values/deferred.rs (2)
  • new (29-46)
  • new (144-157)
crates/napi/src/sendable_resolver.rs (1)
  • new (25-30)
crates/napi/src/js_values/unknown.rs (1)
  • from_raw_unchecked (73-82)
crates/napi/src/js_values/deferred.rs (3)
crates/napi/src/bindgen_runtime/js_values/object.rs (2)
  • finalize_callback (1030-1053)
  • new (716-733)
crates/napi/src/js_values/object.rs (1)
  • finalize_callback (91-112)
crates/napi/src/threadsafe_function.rs (1)
  • new (53-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (43)
  • GitHub Check: Agent
  • GitHub Check: Cursor Bugbot
  • GitHub Check: aarch64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: Test freebsd target
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ 1.88.0
  • GitHub Check: i686-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ stable
  • GitHub Check: i686-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ 1.88.0
  • GitHub Check: aarch64-apple-darwin - node@20 - toolchain@ stable
  • GitHub Check: aarch64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@24 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@20 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@22 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@20 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ stable
  • GitHub Check: build-and-test-msys2 (CLANG64)
  • GitHub Check: Test node wasi target
  • GitHub Check: Test node wasi target (--cfg tokio_unstable)
  • GitHub Check: Build only test - riscv64gc-unknown-linux-gnu
  • GitHub Check: Build only test - armv7-linux-androideabi
  • GitHub Check: Test latest bun
  • GitHub Check: Build only test - aarch64-unknown-linux-ohos
  • GitHub Check: build-and-test-msys2 (UCRT64)
  • GitHub Check: build - s390x-unknown-linux-gnu
  • GitHub Check: stable - armv7-unknown-linux-gnueabihf - node@22
  • GitHub Check: build - x86_64-unknown-linux-gnu
  • GitHub Check: build - aarch64-unknown-linux-gnu
  • GitHub Check: build - powerpc64le-unknown-linux-gnu
  • GitHub Check: Build only test - aarch64-linux-android
  • GitHub Check: build-and-test-msys2 (MINGW64)
  • GitHub Check: Memory leak detect job
  • GitHub Check: Build only test - loongarch64-unknown-linux-gnu
  • GitHub Check: build - x86_64-unknown-linux-musl
  • GitHub Check: build - aarch64-unknown-linux-musl
  • GitHub Check: ASAN - ubuntu-24.04
  • GitHub Check: ASAN - windows-latest
  • GitHub Check: Zig-Cross-Compile-On-Linux (x86_64-unknown-linux-musl)
  • GitHub Check: Zig-Cross-Compile-On-Linux (armv7-unknown-linux-musleabihf)
  • GitHub Check: Zig-Cross-Compile-On-Linux (aarch64-apple-darwin)
  • GitHub Check: Zig-Cross-Compile-On-Linux (aarch64-unknown-linux-musl)
🔇 Additional comments (10)
.github/workflows/test-release.yaml (1)

75-75: LGTM! Leak tests added consistently across primary platforms.

The leak test has been appropriately added to Ubuntu, macOS, and Windows platforms with the stable toolchain. The selective inclusion only for stable toolchain is reasonable, as leak tests don't need to run on every toolchain variant.

Also applies to: 95-95, 106-106

examples/napi/example.wasi.cjs (1)

122-122: LGTM! AsyncThrowClass export properly added.

The export follows the established pattern and is correctly positioned alphabetically. Since this is an auto-generated file, the change appropriately reflects the Rust source modifications.

examples/napi/example.wasi-browser.js (1)

77-77: LGTM! AsyncThrowClass export properly added.

The export follows the established ES module pattern and is correctly positioned alphabetically, consistent with the other binding files.

examples/napi/index.d.cts (1)

109-113: LGTM! AsyncThrowClass type declaration is correct.

The type declaration properly defines the test class for async error scenarios. The constructor and asyncThrowError() method signatures are appropriate for testing the async function memory leak fix described in the PR objectives.

examples/napi/package.json (1)

12-12: The test script is properly configured. The memory-test.ts file exists and contains appropriate leak tests for AsyncThrowClass using WeakRef and garbage collection detection.

examples/napi/index.cjs (1)

590-590: LGTM!

The new AsyncThrowClass export follows the established pattern for exporting native bindings.

examples/napi/__tests__/__snapshots__/values.spec.ts.md (1)

150-154: LGTM!

The type declaration for AsyncThrowClass correctly reflects the Rust implementation with a constructor and an async method returning Promise<void>.

crates/backend/src/codegen/fn.rs (1)

168-172: LGTM! This is the core fix for the memory leak.

The change correctly moves the cleanup of N-API references (_args_ref.drop(env)) into a finalize callback that executes after the async task completes, ensuring proper resource cleanup regardless of success or failure.

crates/napi/src/tokio_runtime.rs (1)

264-264: Verify finalize_callback thread safety.

The finalize_callback is Option<Box<dyn FnOnce(sys::napi_env)>> without Send bounds. This is safe because the callback is invoked via the threadsafe function on the JS thread, but it's worth documenting this invariant.

examples/napi/src/async.rs (1)

46-55: LGTM!

The AsyncThrowClass test fixture is correctly implemented following napi-rs conventions. The struct with #[napi(constructor)] and the impl block with #[napi] properly exports the class to JavaScript. The method intentionally throws an error to test the memory leak fix on error paths.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes a memory leak in async functions by introducing a finalize callback mechanism that ensures proper cleanup of resources when promises are resolved or rejected.

Key changes:

  • Adds a new execute_tokio_future_with_finalize_callback function that accepts a finalize callback for resource cleanup
  • Modifies the JsDeferred struct to store and invoke finalize callbacks during promise resolution/rejection
  • Updates code generation to use the new function and pass a cleanup callback that drops argument references

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/napi/src/tokio_runtime.rs Adds new execute_tokio_future_with_finalize_callback function with finalize callback support
crates/napi/src/js_values/deferred.rs Implements finalize callback storage and invocation in the deferred promise mechanism
crates/backend/src/codegen/fn.rs Updates code generation to use new function and pass cleanup callback for dropping argument references
examples/napi/src/async.rs Adds AsyncThrowClass for testing async error handling and memory leak fix
examples/napi/memory-test.ts Provides memory leak test using WeakRef and garbage collection
examples/napi/package.json Adds test:leak script and @oxc-node/cli dependency
yarn.lock Updates lockfile with new @oxc-node/cli dependency
examples/napi/index.d.cts Adds TypeScript declarations for AsyncThrowClass
examples/napi/index.cjs Exports new AsyncThrowClass
examples/napi/example.wasi.cjs Exports new AsyncThrowClass for WASI
examples/napi/example.wasi-browser.js Exports new AsyncThrowClass for WASI browser
examples/napi/tests/snapshots/ Updates snapshots with new dependencies and type definitions
.github/workflows/test-release.yaml Adds memory leak test to CI workflow

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Brooooooklyn Brooooooklyn force-pushed the 01-08-fix_napi_memory_leak_in_async_fn branch from c6b1128 to a197014 Compare January 8, 2026 12:26
@Brooooooklyn Brooooooklyn force-pushed the 01-08-fix_napi_memory_leak_in_async_fn branch from a197014 to 4601780 Compare January 8, 2026 12:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/napi/src/js_values/deferred.rs (1)

296-320: Finalize callback not invoked when error creation fails.

In the error-of-error path (lines 308-319), when deferred_trace.into_rejected or error creation fails, the finalize callback is not invoked. This could lead to resource leaks as argument references won't be dropped.

Consider adding finalize callback invocation in the Err(err) arm:

🔧 Suggested fix
       Err(err) => {
+        if let Some(finalize_callback) = finalize_callback {
+          finalize_callback(env);
+        }
         if cfg!(debug_assertions) {
           eprintln!("Failed to reject deferred: {err:?}");
           let mut err = ptr::null_mut();
           let mut err_msg = ptr::null_mut();
           unsafe {
             sys::napi_create_string_utf8(env, c"Rejection failed".as_ptr().cast(), 0, &mut err_msg);
             sys::napi_create_error(env, ptr::null_mut(), err_msg, &mut err);
             sys::napi_reject_deferred(env, deferred, err);
           }
         }
       }
🤖 Fix all issues with AI agents
In @crates/napi/src/js_values/deferred.rs:
- Around line 172-178: The current set_finalize_callback replaces the entire Arc
(self.finalize_callback = Arc::new(...)), which means existing clones keep
pointing to the old Arc and won't see updates; instead, acquire a write lock on
the existing RwLock inside self.finalize_callback and replace its inner
Option<Box<dyn FnOnce(sys::napi_env)>> (e.g., call
self.finalize_callback.write() and set *lock = finalize_callback) so all clones
sharing the Arc observe the new callback.

In @crates/napi/src/tokio_runtime.rs:
- Around line 253-320: The function execute_tokio_future_with_finalize_callback
duplicates most logic from execute_tokio_future; refactor by changing
execute_tokio_future to delegate to execute_tokio_future_with_finalize_callback
with finalize_callback = None and remove the duplicated body from
execute_tokio_future so all shared logic lives in
execute_tokio_future_with_finalize_callback (keep resolver handling and
SendableResolver/new spawn/panic handling in the single implementation). Ensure
signatures and generic bounds remain identical and that execute_tokio_future
simply forwards env, fut, and resolver to
execute_tokio_future_with_finalize_callback.
🧹 Nitpick comments (2)
examples/napi/memory-test.ts (1)

13-19: Add a timeout to prevent the test from hanging indefinitely.

The test:leak script correctly includes the --expose-gc flag, so global.gc will be available. However, the test lacks a timeout mechanism to catch cases where the object is not collected, which could cause the test to hang. Add a counter to exit with a non-zero code if garbage collection doesn't occur within a reasonable timeframe.

Suggested implementation
+let attempts = 0;
 setInterval(() => {
   global.gc?.()
   if (wr.deref() === undefined) {
     console.info('No leak')
     process.exit(0)
   }
+  attempts++
+  if (attempts > 30) {
+    console.error('Timeout: Object not collected after 30 seconds')
+    process.exit(1)
+  }
 }, 1000)
examples/napi/src/async.rs (1)

47-56: Consider documenting that this is a test/example class.

While the implementation correctly exercises the memory leak scenario from issues #3086 and #1903, adding a doc comment would clarify its purpose:

📝 Suggested documentation
+/// Test class to demonstrate and verify the fix for memory leaks in async methods
+/// that return errors. See issues #3086 and #1903.
 #[napi(constructor)]
 pub struct AsyncThrowClass {}

 #[napi]
 impl AsyncThrowClass {
   #[napi]
+  /// Always returns an error to exercise the async error path and verify
+  /// that references are properly cleaned up.
   pub async fn async_throw_error(&self) -> Result<()> {
     Err(Error::new(Status::GenericFailure, "Throw async error"))
   }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6b1128 and a197014.

⛔ Files ignored due to path filters (2)
  • examples/napi/__tests__/__snapshots__/values.spec.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/test-release.yaml
  • crates/backend/src/codegen/fn.rs
  • crates/napi/src/js_values/deferred.rs
  • crates/napi/src/tokio_runtime.rs
  • examples/napi/__tests__/__snapshots__/values.spec.ts.md
  • examples/napi/example.wasi-browser.js
  • examples/napi/example.wasi.cjs
  • examples/napi/index.cjs
  • examples/napi/index.d.cts
  • examples/napi/memory-test.ts
  • examples/napi/package.json
  • examples/napi/src/async.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • examples/napi/index.d.cts
  • examples/napi/index.cjs
  • examples/napi/package.json
  • examples/napi/example.wasi.cjs
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `yarn workspace examples/napi build` before testing to ensure code generation reflects current Rust changes
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi]` attribute on impl blocks to define classes exported to JavaScript/TypeScript

Applied to files:

  • crates/backend/src/codegen/fn.rs
  • examples/napi/example.wasi-browser.js
  • examples/napi/src/async.rs
  • crates/napi/src/js_values/deferred.rs
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `yarn workspace examples/napi build` before testing to ensure code generation reflects current Rust changes

Applied to files:

  • crates/backend/src/codegen/fn.rs
  • examples/napi/memory-test.ts
  • examples/napi/src/async.rs
  • .github/workflows/test-release.yaml
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `cargo test` for Rust unit tests and `yarn workspace examples/napi test` for integration tests

Applied to files:

  • examples/napi/memory-test.ts
  • .github/workflows/test-release.yaml
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi(js_name = "...")]` attribute to rename Rust items in JavaScript/TypeScript

Applied to files:

  • examples/napi/src/async.rs
  • crates/napi/src/js_values/deferred.rs
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Rebuild TypeScript definitions after making changes to Rust code using `yarn workspace examples/napi build`

Applied to files:

  • .github/workflows/test-release.yaml
🧬 Code graph analysis (4)
crates/backend/src/codegen/fn.rs (1)
crates/napi/src/tokio_runtime.rs (3)
  • execute_tokio_future_with_finalize_callback (256-320)
  • execute_tokio_future_with_finalize_callback (324-335)
  • new (350-355)
crates/napi/src/tokio_runtime.rs (6)
crates/napi/src/bindgen_runtime/js_values/object.rs (2)
  • finalize_callback (1030-1053)
  • from_raw (704-713)
crates/napi/src/js_values/object.rs (1)
  • finalize_callback (91-112)
crates/napi/src/env.rs (2)
  • from_raw (72-74)
  • spawn (1041-1046)
crates/napi/src/js_values/deferred.rs (2)
  • new (29-46)
  • new (146-159)
crates/napi/src/sendable_resolver.rs (1)
  • new (25-30)
crates/napi/src/js_values/unknown.rs (1)
  • from_raw_unchecked (73-82)
examples/napi/src/async.rs (2)
examples/napi/example.wasi-browser.js (4)
  • AsyncThrowClass (77-77)
  • AsyncThrowClass (77-77)
  • Status (357-357)
  • Status (357-357)
crates/napi/src/bindgen_runtime/callback_info.rs (1)
  • new (27-80)
crates/napi/src/js_values/deferred.rs (2)
crates/napi/src/bindgen_runtime/js_values/object.rs (2)
  • finalize_callback (1030-1053)
  • new (716-733)
crates/napi/src/threadsafe_function.rs (1)
  • new (53-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (41)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: x86_64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@20 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@20 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ 1.88.0
  • GitHub Check: i686-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ stable
  • GitHub Check: aarch64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@20 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ 1.88.0
  • GitHub Check: i686-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: aarch64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@22 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@24 - toolchain@ stable
  • GitHub Check: build - powerpc64le-unknown-linux-gnu
  • GitHub Check: build - aarch64-unknown-linux-gnu
  • GitHub Check: Build only test - loongarch64-unknown-linux-gnu
  • GitHub Check: build-and-test-msys2 (MINGW64)
  • GitHub Check: build - x86_64-unknown-linux-gnu
  • GitHub Check: Build only test - riscv64gc-unknown-linux-gnu
  • GitHub Check: Test node wasi target (--cfg tokio_unstable)
  • GitHub Check: build - x86_64-unknown-linux-musl
  • GitHub Check: Build only test - aarch64-linux-android
  • GitHub Check: build-and-test-msys2 (UCRT64)
  • GitHub Check: build-and-test-msys2 (CLANG64)
  • GitHub Check: build - aarch64-unknown-linux-musl
  • GitHub Check: Build only test - armv7-linux-androideabi
  • GitHub Check: Build only test - aarch64-unknown-linux-ohos
  • GitHub Check: stable - armv7-unknown-linux-gnueabihf - node@22
  • GitHub Check: build - s390x-unknown-linux-gnu
  • GitHub Check: Test node wasi target
  • GitHub Check: Test freebsd target
  • GitHub Check: Memory leak detect job
  • GitHub Check: ASAN - ubuntu-24.04
  • GitHub Check: ASAN - windows-latest
  • GitHub Check: Zig-Cross-Compile-On-Linux (armv7-unknown-linux-musleabihf)
  • GitHub Check: Zig-Cross-Compile-On-Linux (x86_64-unknown-linux-musl)
  • GitHub Check: Zig-Cross-Compile-On-Linux (aarch64-apple-darwin)
  • GitHub Check: Zig-Cross-Compile-On-Linux (aarch64-unknown-linux-musl)
🔇 Additional comments (18)
.github/workflows/test-release.yaml (3)

75-75: LGTM! Leak test added to primary CI workflow.

The leak test is appropriately placed after the main test suite and before type checking. This ensures memory leak validation is part of the standard CI pipeline for the stable toolchain on Ubuntu.


95-95: LGTM! Consistent leak test coverage for macOS.


106-106: test:leak script is properly defined and integrated.

The test:leak script is correctly defined in examples/napi/package.json as "oxnode --expose-gc ./memory-test.ts" and is appropriately used in the Windows workflow.

crates/napi/src/js_values/deferred.rs (6)

3-6: LGTM! Well-designed finalize callback type.

The FinalizeCallback type correctly uses Arc<RwLock<Option<Box<dyn FnOnce(...)>>>> to enable:

  • Thread-safe shared ownership (Arc)
  • Interior mutability for taking the callback (RwLock)
  • Single invocation semantics (FnOnce)
  • Optional callback support (Option)

Also applies to: 104-104


111-111: LGTM! Finalize callback properly integrated into data structures.

The finalize callback is correctly:

  • Added to both DeferredData (for transport) and JsDeferred (for storage)
  • Cloned in the Clone implementation (Arc ref-count increment)
  • Initialized with Default::default() (None callback by default)

Also applies to: 118-118, 133-133, 153-153


180-201: LGTM! Finalize callback properly cloned into DeferredData.

The Arc clone at line 186 correctly shares the finalize callback with the thread-safe function callback data.


262-265: LGTM! Finalize callback correctly consumed.

Lines 263-265 properly take the finalize callback using RwLock::write().take(), ensuring single invocation semantics. The .expect("RwLock Poison") will panic if the lock is poisoned, which is appropriate as lock poisoning indicates a serious runtime error.


304-306: LGTM! Finalize callback invoked on error path.

The finalize callback is correctly invoked after rejecting the deferred promise on the error path.


321-323: LGTM! Finalize callback invoked on success path.

The finalize callback is correctly invoked after successfully resolving the deferred promise.

examples/napi/example.wasi-browser.js (1)

77-77: LGTM! AsyncThrowClass properly exposed in WASI browser wrapper.

The export follows the established pattern and is correctly positioned alphabetically.

examples/napi/memory-test.ts (3)

1-1: LGTM! Import statement correct.


3-9: LGTM! Test function correctly implements leak detection pattern.

The function creates an instance, triggers the async error, and returns a WeakRef for GC detection. The .catch() handler properly handles promise rejection.


11-11: LGTM! Test invocation correct.

crates/backend/src/codegen/fn.rs (1)

168-173: LGTM! Finalize callback correctly ensures argument cleanup.

This change is the core fix for the memory leak described in #3086 and #1903. By moving _args_ref.drop() into the finalize callback, N-API references are now properly released on all code paths (success, error, and panic), rather than being leaked when async functions throw errors.

The finalize callback correctly:

  • Captures _args_ref with move closure
  • Invokes drop(env) to release N-API references
  • Executes after promise resolution/rejection
examples/napi/__tests__/__snapshots__/values.spec.ts.md (1)

15-15: LGTM! Snapshot correctly reflects the new test infrastructure.

The snapshot correctly captures:

  • The new @oxc-node/cli dependency for leak testing
  • The new AsyncThrowClass type definition matching the Rust implementation

Also applies to: 150-154

crates/napi/src/tokio_runtime.rs (3)

322-335: LGTM! Noop stub correctly implements feature-gated fallback.

The noop implementation is consistent with the existing execute_tokio_future noop stub and correctly provides a no-op fallback when the noop feature is enabled.


267-273: No action needed—finalize callback behavior is correct by design.

The finalize_callback is wrapped in Arc<RwLock<Option<_>>> and shared across clones. When either deferred or deferred_for_panic resolves/rejects, it calls .take() on the RwLock, which atomically extracts the callback and leaves None for subsequent clones. This ensures the callback is invoked exactly once, preventing both double-invocation and resource leaks. The clone pattern is intentional for panic handling, as noted in the code comment.


312-317: The finalize callback execution is thread-safe and does not require changes. The callback is invoked on the JavaScript thread via the threadsafe function mechanism, not on the spawned worker thread.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
crates/napi/src/js_values/deferred.rs (1)

263-265: Consider adding context to the expect message.

The expect("RwLock Poison") will only trigger if a prior panic occurred while holding the lock. While acceptable, a more descriptive message could aid debugging.

Suggested improvement
-  let finalize_callback = RwLock::write(&deferred_data.finalize_callback)
-    .expect("RwLock Poison")
-    .take();
+  let finalize_callback = RwLock::write(&deferred_data.finalize_callback)
+    .expect("finalize_callback RwLock poisoned by prior panic")
+    .take();
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a197014 and 4601780.

⛔ Files ignored due to path filters (2)
  • examples/napi/__tests__/__snapshots__/values.spec.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .github/workflows/test-release.yaml
  • crates/backend/src/codegen/fn.rs
  • crates/napi/src/js_values/deferred.rs
  • crates/napi/src/tokio_runtime.rs
  • examples/napi/__tests__/__snapshots__/values.spec.ts.md
  • examples/napi/example.wasi-browser.js
  • examples/napi/example.wasi.cjs
  • examples/napi/index.cjs
  • examples/napi/index.d.cts
  • examples/napi/memory-test.ts
  • examples/napi/package.json
  • examples/napi/src/async.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • examples/napi/example.wasi.cjs
  • .github/workflows/test-release.yaml
  • examples/napi/package.json
  • examples/napi/tests/snapshots/values.spec.ts.md
  • examples/napi/memory-test.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `yarn workspace examples/napi build` before testing to ensure code generation reflects current Rust changes
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi]` attribute on impl blocks to define classes exported to JavaScript/TypeScript

Applied to files:

  • examples/napi/src/async.rs
  • crates/backend/src/codegen/fn.rs
  • examples/napi/example.wasi-browser.js
  • crates/napi/src/js_values/deferred.rs
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Run `yarn workspace examples/napi build` before testing to ensure code generation reflects current Rust changes

Applied to files:

  • examples/napi/src/async.rs
  • crates/backend/src/codegen/fn.rs
📚 Learning: 2025-11-25T09:31:23.877Z
Learnt from: CR
Repo: napi-rs/napi-rs PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:31:23.877Z
Learning: Applies to /crates/**/*.rs : Use `#[napi(js_name = "...")]` attribute to rename Rust items in JavaScript/TypeScript

Applied to files:

  • examples/napi/src/async.rs
  • crates/napi/src/js_values/deferred.rs
🧬 Code graph analysis (4)
examples/napi/src/async.rs (4)
crates/macro/src/lib.rs (1)
  • napi (27-41)
examples/napi/example.wasi-browser.js (4)
  • AsyncThrowClass (77-77)
  • AsyncThrowClass (77-77)
  • Status (357-357)
  • Status (357-357)
crates/napi/src/js_values/deferred.rs (2)
  • new (29-46)
  • new (146-159)
crates/napi/src/bindgen_runtime/callback_info.rs (1)
  • new (27-80)
crates/backend/src/codegen/fn.rs (1)
crates/napi/src/tokio_runtime.rs (2)
  • execute_tokio_future_with_finalize_callback (256-320)
  • execute_tokio_future_with_finalize_callback (324-335)
examples/napi/index.d.cts (1)
examples/napi/example.wasi-browser.js (2)
  • AsyncThrowClass (77-77)
  • AsyncThrowClass (77-77)
crates/napi/src/js_values/deferred.rs (2)
crates/napi/src/bindgen_runtime/js_values/object.rs (2)
  • finalize_callback (1030-1053)
  • new (716-733)
crates/napi/src/threadsafe_function.rs (1)
  • new (53-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: Test Zig Cross Compiled aarch64-apple-darwin
  • GitHub Check: x86_64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@20 - toolchain@ stable
  • GitHub Check: x86_64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: i686-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: aarch64-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ 1.88.0
  • GitHub Check: x86_64-pc-windows-msvc - node@20 - toolchain@ stable
  • GitHub Check: i686-pc-windows-msvc - node@22 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ stable
  • GitHub Check: aarch64-pc-windows-msvc - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@24 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@24 - toolchain@ stable
  • GitHub Check: x86_64-unknown-linux-gnu - node@22 - toolchain@ 1.88.0
  • GitHub Check: aarch64-apple-darwin - node@20 - toolchain@ stable
  • GitHub Check: aarch64-apple-darwin - node@22 - toolchain@ stable
  • GitHub Check: Test freebsd target
  • GitHub Check: build-and-test-msys2 (CLANG64)
  • GitHub Check: build-and-test-msys2 (UCRT64)
  • GitHub Check: build-and-test-msys2 (MINGW64)
  • GitHub Check: build - powerpc64le-unknown-linux-gnu
  • GitHub Check: Build only test - aarch64-linux-android
  • GitHub Check: stable - armv7-unknown-linux-gnueabihf - node@22
  • GitHub Check: Memory leak detect job
  • GitHub Check: ASAN - ubuntu-24.04
  • GitHub Check: ASAN - windows-latest
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (9)
crates/backend/src/codegen/fn.rs (1)

168-172: Correct fix for the async memory leak.

The change properly ensures _args_ref (the NapiRefContainer holding N-API references) is dropped via the finalize callback after async completion, regardless of whether the promise resolves or rejects. This addresses the root cause of issues #3086 and #1903.

examples/napi/src/async.rs (1)

46-56: Good test fixture for memory leak verification.

This class provides a minimal reproduction case that always rejects the async promise, which is essential for testing that N-API references are properly released on the error path.

crates/napi/src/js_values/deferred.rs (2)

104-104: Appropriate type design for the finalize callback.

The Arc<RwLock<Option<...>>> pattern is correctly chosen:

  • Arc enables sharing across the clone for panic handling
  • RwLock provides interior mutability to take() the callback
  • Option ensures the callback is consumed exactly once

304-326: Finalize callback correctly invoked on all exit paths.

The callback is properly invoked in all three scenarios:

  1. Successful resolution (line 324-326)
  2. Rejection with valid error value (line 304-306)
  3. Rejection with fallback error on error creation failure (line 309-311)

This ensures N-API references are always cleaned up, fixing the memory leak.

crates/napi/src/tokio_runtime.rs (2)

253-320: Well-implemented finalize callback support.

The new function correctly:

  1. Sets the finalize callback on the deferred before spawning
  2. Clones the deferred for panic handling (sharing the same Arc<RwLock<...>>)
  3. Ensures the callback is invoked regardless of success, error, or panic

The mirroring of execute_tokio_future structure makes the code easy to follow and maintain.


322-335: Noop stub correctly implemented.

The stub returns Ok(std::ptr::null_mut()) and ignores the finalize callback, consistent with the existing execute_tokio_future noop pattern.

examples/napi/index.cjs (1)

590-590: Auto-generated export correctly added.

The AsyncThrowClass export follows the established pattern and is correctly positioned alphabetically.

examples/napi/example.wasi-browser.js (1)

77-77: Auto-generated WASI browser export correctly added.

The export is correctly positioned and follows the established pattern.

examples/napi/index.d.cts (1)

109-113: Auto-generated TypeScript declaration correctly added.

The declaration accurately reflects the Rust implementation:

  • Empty constructor matching the unit struct
  • asyncThrowError(): Promise<void> matching async fn async_throw_error(&self) -> Result<()>

@Brooooooklyn Brooooooklyn merged commit e51f320 into main Jan 8, 2026
76 checks passed
@Brooooooklyn Brooooooklyn deleted the 01-08-fix_napi_memory_leak_in_async_fn branch January 8, 2026 12:46
@github-actions github-actions bot mentioned this pull request Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Throwing from an async method leaks memory Memory leak in napi::Error::from_reason

1 participant