Skip to content

fix(napi): skip nullish error causes when converting from Unknown#3143

Merged
Brooooooklyn merged 7 commits into
napi-rs:mainfrom
jeiea:feat/nullish-cause-guard
Mar 17, 2026
Merged

fix(napi): skip nullish error causes when converting from Unknown#3143
Brooooooklyn merged 7 commits into
napi-rs:mainfrom
jeiea:feat/nullish-cause-guard

Conversation

@jeiea

@jeiea jeiea commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

Background

I found this while cleaning up build failure handling in an unplugin-based workflow. The original goal was to make build failures propagate by throwing, but in deno that error did not only surface as an expected build failure and was also observed as an extra unhandledrejection.

At first I suspected error propagation in unplugin or rolldown, and I narrowed the reproduction down from that direction. However, the issue turned out to be lower in the stack. While converting rejected values across the napi boundary into napi::Error, Error.cause was still being recursively converted even when the cause was undefined or null.

That behavior can be easy to miss in practice, but on some host runtimes, specifically Deno, it can leave an unnecessary pending exception behind, which later becomes observable as unhandledrejection. Because of that, it seemed more appropriate to tighten the Unknown -> Error conversion policy in napi-rs first instead of treating this as primarily a rolldown or Deno issue.

This change captures the host-independent rule behind that fix: nullish causes should not be reconstructed as nested errors.

Changes

  • Skip recursive conversion when Error.cause is undefined or null.

Notes for Reviewers

The observable failure here is host-specific, but this PR intentionally stops one level earlier and focuses on the policy that napi-rs itself should guarantee.

Because of that, the added tests do not prove that a host runtime no longer leaves a pending exception behind. Instead, they verify the narrower rule that nullish causes are not treated as nested errors. I plan to attach a separate reproduction script for the runtime-level behavior in the PR discussion rather than include it in this commit.

The main review question is whether skipping undefined and null causes is the right default for napi::Error's error model.

Testing

  • yarn workspace @examples/napi build
  • cargo test -p napi --lib
deno -ER --allow-ffi examples/napi/repro-deno-unhandledrejection.mjs
import binding from './index.cjs'

let unhandledRejectionCount = 0

const onUnhandledRejection = (event) => {
  unhandledRejectionCount += 1
  console.error('[unhandledrejection]', event.reason)
  event.preventDefault?.()
}

globalThis.addEventListener('unhandledrejection', onUnhandledRejection)

const runCase = async ({ label, cause }) => {
  try {
    await binding.asyncPlus100(Promise.reject(new Error(`outer-${label}`, { cause })))
  } catch (error) {
    console.log('[caught]', error?.message, 'cause=', error?.cause)
  }
}

await runCase({ label: 'undefined', cause: undefined })
await runCase({ label: 'null', cause: null })
await runCase({ label: 'error', cause: new Error('inner') })

globalThis.removeEventListener('unhandledrejection', onUnhandledRejection)

if (unhandledRejectionCount > 0) {
  console.error(
    `reproduced: ${unhandledRejectionCount} unhandledrejection event(s) observed`,
  )
  Deno.exit(1)
}

console.log('ok: no unhandledrejection observed')

Summary by CodeRabbit

  • Improvements

    • Unified cause extraction logic across environments for consistent behavior and reduced duplication.
    • Streamlined control flow so failures during cause determination are correctly propagated and handled.
  • Tests

    • Added unit tests verifying when causes are skipped or extracted (nullish vs non-nullish) and that lookup errors propagate as expected.

@coderabbitai

coderabbitai Bot commented Mar 7, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces centralized error-cause extraction in crates/napi/src/error.rs via extract_error_cause() and should_extract_error_cause(), refactors both WASM and non-WASM From<Unknown> implementations to use these helpers, and adds unit tests for extraction behavior and lookup error propagation.

Changes

Cohort / File(s) Summary
Error cause extraction & tests
crates/napi/src/error.rs
Adds extract_error_cause(value) -> Result<Option<Box<Error>>> and should_extract_error_cause(cause_type) -> Result<bool>; refactors WASM and non-WASM From<Unknown> to use shared helpers (removing inline extraction logic); adds #[cfg(test)] tests covering nullish vs non-nullish causes and lookup error propagation; updates imports related to error extraction.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I nibble code where causes hide,

pulled tidy trails from every side.
Tests hop in to prove the chase,
one clever extractor, snug in place.
🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 concisely describes the main change: skipping nullish error causes in the Unknown to Error conversion process.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can use OpenGrep to find security vulnerabilities and bugs across 17+ programming languages.

OpenGrep is compatible with Semgrep configurations. Add an opengrep.yml or semgrep.yml configuration file to your project to enable OpenGrep analysis.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

695-717: Add one test on the actual From<Unknown> path.

These tests pin the nullish predicate, but they would still pass if extract_error_cause regressed in the object/property-access path. A small end-to-end case asserting that { cause: undefined } and { cause: null } produce Error { cause: None, .. } would make this change much harder to break later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/napi/src/error.rs` around lines 695 - 717, Add an end-to-end test that
exercises the From<Unknown> conversion path: construct an Unknown representing
an object with a "cause" property set to Undefined and another with Null,
convert each via From<Unknown> into the crate's Error type (the same path that
calls extract_error_cause/should_extract_error_cause), and assert that the
resulting Error.cause is None in both cases; place this new test alongside the
existing tests in the tests module and reference the From<Unknown> conversion,
extract_error_cause, and Error type so the object/property-access path is
covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/napi/src/error.rs`:
- Around line 671-676: In extract_error_cause, the call to
obj.get_named_property::<Unknown>("cause").ok() swallows N-API errors (including
Status::PendingException) and leaves pending JS exceptions uncleared; update
extract_error_cause to stop discarding errors: either propagate the error
(return Err) when get_named_property fails or call the helper macro
check_pending_exception! (or equivalent) to clear the pending exception before
returning None, ensuring pending exceptions are not left dangling; update the
logic around extract_error_cause and the get_named_property("cause") call
accordingly and add an end-to-end test that exercises the property-access path
(a getter/proxy that throws) to verify error conversion and clearing of pending
exceptions.

---

Nitpick comments:
In `@crates/napi/src/error.rs`:
- Around line 695-717: Add an end-to-end test that exercises the From<Unknown>
conversion path: construct an Unknown representing an object with a "cause"
property set to Undefined and another with Null, convert each via From<Unknown>
into the crate's Error type (the same path that calls
extract_error_cause/should_extract_error_cause), and assert that the resulting
Error.cause is None in both cases; place this new test alongside the existing
tests in the tests module and reference the From<Unknown> conversion,
extract_error_cause, and Error type so the object/property-access path is
covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10346c8a-eb7f-4510-a777-3020d3cc97d1

📥 Commits

Reviewing files that changed from the base of the PR and between 87410c5 and f6e8cf0.

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

Comment thread crates/napi/src/error.rs Outdated
Comment thread crates/napi/src/error.rs Outdated
@Brooooooklyn

Copy link
Copy Markdown
Member

@cursor review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/napi/src/error.rs`:
- Around line 677-680: In extract_error_cause, don't hide errors from
value.get_type(): replace the matches!(value.get_type(), Ok(ValueType::Object))
check with a call that propagates failures (e.g. let ty = value.get_type()?; if
ty != ValueType::Object { return Ok(None); }) so actual get_type() errors (like
pending JS exceptions) are returned instead of being treated as Ok(None); keep
the rest of extract_error_cause unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05656e18-a272-4d05-9fb2-2593c3a770ed

📥 Commits

Reviewing files that changed from the base of the PR and between f49f87d and bdebeeb.

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

Comment thread crates/napi/src/error.rs
@jeiea jeiea marked this pull request as draft March 8, 2026 06:54
@jeiea jeiea force-pushed the feat/nullish-cause-guard branch from bdebeeb to 9c08ca1 Compare March 8, 2026 07:05
jeiea added 4 commits March 8, 2026 16:05
Avoid recursively converting `Error.cause` when it is `undefined` or `null`.

This prevents host runtimes from observing spurious pending exceptions
when `Unknown -> Error` conversion inspects nullable causes.
@jeiea jeiea force-pushed the feat/nullish-cause-guard branch from 9c08ca1 to ecec35d Compare March 8, 2026 07:06
@jeiea jeiea marked this pull request as ready for review March 8, 2026 07:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/napi/src/error.rs`:
- Line 16: Remove the unused import JsObjectValue from the top of error.rs:
delete the line "use crate::bindgen_runtime::JsObjectValue;" since the file uses
Object::get_named_property() and Unknown-based methods (no JsObjectValue trait
methods), so eliminating this import removes the unused dependency and cleans up
the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f5aa65e-7995-4e19-ac5e-3f2266787a87

📥 Commits

Reviewing files that changed from the base of the PR and between ecec35d and d52968f.

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

Comment thread crates/napi/src/error.rs
@jeiea

jeiea commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

One check is failing, but this isn't a problem with this PR; it seems to have been happening for a while.

Brooooooklyn and others added 2 commits March 17, 2026 12:20
- Move cause extraction after reference creation in non-WASM path to
  preserve original ordering
- Use unwrap_or(None) instead of returning cause-extraction errors,
  so the original error is never lost
- Inline should_extract_error_cause into extract_error_cause
- Replace unit tests with integration tests for nullish cause handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Brooooooklyn Brooooooklyn merged commit afb6f67 into napi-rs:main Mar 17, 2026
72 of 73 checks passed
@github-actions github-actions Bot mentioned this pull request Mar 17, 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.

2 participants