Skip to content

fix(napi): handle ThreadsafeFunction callback errors gracefully during shutdown#3188

Merged
Brooooooklyn merged 1 commit intomainfrom
fix/tsfn-shutdown-panic
Apr 3, 2026
Merged

fix(napi): handle ThreadsafeFunction callback errors gracefully during shutdown#3188
Brooooooklyn merged 1 commit intomainfrom
fix/tsfn-shutdown-panic

Conversation

@Brooooooklyn
Copy link
Copy Markdown
Member

@Brooooooklyn Brooooooklyn commented Apr 3, 2026

When a Node.js environment is tearing down (e.g. Ctrl+C during watch mode
in a worker thread), NAPI calls can fail with unexpected status codes.

Reported in rolldown/rolldown#8951: pressing Ctrl+C while rolldown's watch
mode is performing its initial build causes a reproducible panic in napi-rs.

The panic occurs in handle_call_js_cb_status in the else branch, which
handles status codes that are neither napi_ok nor napi_pending_exception.
The full control flow is:

  1. call_js_cb is the C callback invoked by Node's ThreadsafeFunction
    machinery on the main/JS thread.
  2. It calls napi_call_function to invoke the JS callback, or
    napi_fatal_exception when the Rust callback errors.
  3. The return status flows to handle_call_js_cb_status(status, raw_env).
  4. If status is napi_pending_exception, the function clears the exception
    and calls napi_fatal_exception — this path already tolerates
    napi_pending_exception as a return from napi_fatal_exception.
  5. For any other non-ok status, the else branch constructs a JS Error
    object (via napi_create_string_utf8 × 2 + napi_create_error) and
    calls napi_fatal_exception. Every one of these calls used assert_eq!
    against napi_ok, which panics during environment teardown.

During shutdown, napi_fatal_exception returns napi_pending_exception
(status 10) instead of napi_ok, and the string/error construction calls
can also fail. The fix:

  • Replace assert_eq! with early returns on napi_create_string_utf8 and
    napi_create_error — if we can't even construct the error object in a
    half-torn-down environment, there's nothing useful we can do.
  • Tolerate napi_pending_exception from napi_fatal_exception, matching
    the pattern already used in the napi_pending_exception branch above.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com


Note

Medium Risk
Changes error-handling behavior in ThreadsafeFunction callbacks by replacing panicking assertions with early returns, which could mask some unexpected failures but should prevent crashes during teardown.

Overview
Prevents panics in handle_call_js_cb_status when ThreadsafeFunction callback invocation fails during Node environment shutdown.

Instead of assert_eq!-ing N-API calls to napi_ok while constructing a JS Error, the code now bails out early if napi_create_string_utf8/napi_create_error fail, and it tolerates napi_pending_exception from napi_fatal_exception (matching the existing pending-exception path).

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

Summary by CodeRabbit

  • Refactor
    • Improved error handling in asynchronous operation management by replacing assertions with conditional checks, allowing graceful failure recovery instead of panicking.

…g shutdown

When a Node.js environment is tearing down (e.g. Ctrl+C during watch mode
in a worker thread), NAPI calls can fail with unexpected status codes.

Reported in rolldown/rolldown#8951: pressing Ctrl+C while rolldown's watch
mode is performing its initial build causes a reproducible panic in napi-rs.

The panic occurs in `handle_call_js_cb_status` in the `else` branch, which
handles status codes that are neither `napi_ok` nor `napi_pending_exception`.
The full control flow is:

1. `call_js_cb` is the C callback invoked by Node's ThreadsafeFunction
   machinery on the main/JS thread.
2. It calls `napi_call_function` to invoke the JS callback, or
   `napi_fatal_exception` when the Rust callback errors.
3. The return status flows to `handle_call_js_cb_status(status, raw_env)`.
4. If status is `napi_pending_exception`, the function clears the exception
   and calls `napi_fatal_exception` — this path already tolerates
   `napi_pending_exception` as a return from `napi_fatal_exception`.
5. For any other non-ok status, the `else` branch constructs a JS Error
   object (via `napi_create_string_utf8` × 2 + `napi_create_error`) and
   calls `napi_fatal_exception`. Every one of these calls used `assert_eq!`
   against `napi_ok`, which panics during environment teardown.

During shutdown, `napi_fatal_exception` returns `napi_pending_exception`
(status 10) instead of `napi_ok`, and the string/error construction calls
can also fail. The fix:

- Replace `assert_eq!` with early returns on `napi_create_string_utf8` and
  `napi_create_error` — if we can't even construct the error object in a
  half-torn-down environment, there's nothing useful we can do.
- Tolerate `napi_pending_exception` from `napi_fatal_exception`, matching
  the pattern already used in the `napi_pending_exception` branch above.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0db43ed1-9f50-4263-8c9c-53fc6de618a3

📥 Commits

Reviewing files that changed from the base of the PR and between f10d6ce and 9a7d9cc.

📒 Files selected for processing (1)
  • crates/napi/src/threadsafe_function.rs

📝 Walkthrough

Walkthrough

Error handling in the handle_call_js_cb_status function was modified to use conditional checks instead of assertions for N-API operations. When N-API calls fail, the function now returns early rather than panicking, and the final assertion was updated to accept either napi_ok or napi_pending_exception.

Changes

Cohort / File(s) Summary
Error Handling in N-API Callbacks
crates/napi/src/threadsafe_function.rs
Replaced panic-inducing assertions with conditional error checks on N-API string and error creation calls; early returns on failure and softened final assertion to permit napi_pending_exception alongside napi_ok.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 No more panics when N-API calls fail,
We gracefully check and return without wail,
Assertions now soften, no exceptions to crash,
Each error path handled with defensive stash! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: fixing graceful error handling for ThreadsafeFunction callbacks during shutdown, which directly matches the core objective of preventing panics from assertion failures.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tsfn-shutdown-panic

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
Copy Markdown
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 changes, fast-track this PR to the front of the merge queue

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
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread crates/napi/src/threadsafe_function.rs
@Brooooooklyn Brooooooklyn merged commit 8a87b7a into main Apr 3, 2026
75 checks passed
@Brooooooklyn Brooooooklyn deleted the fix/tsfn-shutdown-panic branch April 3, 2026 10:04
@github-actions github-actions Bot mentioned this pull request Apr 3, 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.

1 participant