Skip to content

Fix N-API error builders returning an unwritten error value - #906

Merged
kriszyp merged 7 commits into
mainfrom
fix/create-rocksdb-error-return-value
Oct 6, 2026
Merged

kriszyp merged 7 commits into
mainfrom
fix/create-rocksdb-error-return-value

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

createRocksDBError and createJSError (src/binding/napi/helpers.cpp) return their result through an error out-param. When one of their own N-API calls failed, they returned without writing it. About 17 call sites never initialise that local, so on that failure it reached napi_throw or a promise reject unwritten. The async completion sites (backup, backup stream, checkpoint, commit) would then never settle their promise.

❓ Your call: I fixed the builders, not the call sites. The dispatch task preferred changing the builders to return a napi_value and updating all 22 callers; I did not, because a napi_value return can also be null, so it gives no guarantee the out-param lacks once the builder always writes. Reviewers agreed in plan review. Say if you want the return-value form anyway.

💡 Solution

When an internal N-API call fails, the builder hands back the exception that call left pending. If none is pending, it synthesizes one first, so the value is always a real JS value. Every path now writes error, so no caller changes. Successful builds gain no N-API calls.

Invariant: after either builder returns, error is a valid napi_value. The contract is in src/binding/napi/DESIGN.md.

⚖️ Alternatives

  • Return napi_value and migrate 22 call sites. Not chosen: same guarantee as the out-param once the builder always writes it; 22 files changed for no added guarantee.
  • Initialise and check at each call site. Not chosen: the async sites have no napi_throw fallback, so each would need its own recovery; the builder contract would stay "may leave the out-param unwritten", so the next caller repeats the bug.

Framing-Verdict: better-alternative-exists (34abdcc9efcd) from the planning review, adopted: the centralized out-param repair, as the reviewer proposed.

🔧 Changes

❓ Your call: Not fixed here, recorded as a follow-up. Commit-metadata decorations (hasLog at transaction.cpp:856 and its sync siblings, appliedInMemoryOnly at database.cpp) set a property on the recovered value without checking the result. If that value is null or undefined, the setter throws and the commit promise is never settled. A builder returns undefined only when it cannot create an exception (out-of-memory during napi_create_error) or when user code makes Object.create throw null or undefined. Fixing it means guarding four decoration sites, which is a separate change.

✅ Verification

Pre-push review: 7 rounds on the pre-rebase head, then one full round on the rebased head. The open findings are the decoration hazard above, plus follow-ups recorded in the dispatch file: a lost RocksDB status message when the builder itself fails under OOM, the duplicated Object.create/Error.prototype lookup, and the now-dead error == nullptr guards in database.cpp.

🤖 Generated with Claude Code

Related PRs: #767 independent, #905 overlaps (DESIGN.md index line), #897 overlaps (README and DESIGN.md index lines), #742 independent, #902 overlaps (README and DESIGN.md index lines), #901 overlaps (DESIGN.md index line), #890 overlaps (DESIGN.md index line)
Complexity: medium

Dispatch: task rocksdb-js-createrocksdberror-callsites · queued by unknown · ran by claude/sonnet/xhigh · worker kzyp-xps-1

Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-kimi,cursor-muse; rounds=8; full=4 @ bd9ff25

Review-Attention: read ~3m (decisions: recovered-value-shape, synthesized-message, fix-layer) @ bd9ff25

kriszyp and others added 7 commits October 6, 2026 11:28
createRocksDBError and createJSError returned without writing `error` when
one of their own N-API calls failed. Several callers never initialised the
local, so it reached napi_throw or a promise reject unwritten.

On a failed N-API call the builder now takes back the exception that call
left pending and returns it as the error value. Callers are unchanged.

Also correct the README Node.js requirement to match package.json engines.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
The builders threw a synthesized exception before taking the pending one.
Node keeps the original value, but a runtime that overwrites a pending
exception would hand callers the synthesized one. Synthesize only when
nothing is pending.

Cover the sync createJSError path too, and drop the stderr assertion from
the regression test, which failed on unrelated warnings.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
A thrown Object.create value is already pending, so napi_throw on the
uninitialised local silently re-throws it and the sync case passed with
the fix reverted. Use a non-callable factory, which fails without a JS
exception, for the sync case. Also destroy the fixture database in a
finally.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
The sync createJSError case passes on the pre-fix builder as well, because
the synthesized exception is already pending and the caller's napi_throw is
a no-op. Cite the async case as the regression.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
A timed-out child is killed before its finally block runs, so the parent
removes the database directory after the child exits. Reword the helper
comment to state why the synthesis is conditional.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
napi_is_exception_pending resets the last-error info, so the message read
after it lost the failing call's text. Read it first. The fixture now waits
for the killed child to exit before the parent removes its database. The
design note records that napi_get_and_clear_last_exception does not fail on
a live env.

Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV
Dispatch-Task: rocksdb-js-createrocksdberror-callsites
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Nbnowm6yc4yedPsRJqZmV

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates the N-API error builders (createRocksDBError and createJSError in helpers.cpp) to ensure they always write to their error out-parameter, even when internal N-API calls fail. It introduces a new helper takeFailedCallException and macro NAPI_STATUS_TAKES_ERROR to retrieve or synthesize pending exceptions correctly. Additionally, the Node.js version requirement in README.md is updated to ^22.18.0 || >=24.0.0, design documentation is added, and integration tests are introduced to verify error handling behavior. No review comments were provided, so there is no feedback to address.

@kriszyp
kriszyp marked this pull request as ready for review October 6, 2026 17:38
@kriszyp
kriszyp requested a review from cb1kenobi as a code owner October 6, 2026 17:38
@kriszyp
kriszyp force-pushed the fix/create-rocksdb-error-return-value branch from 2406269 to bd9ff25 Compare October 6, 2026 17:56
@kriszyp
kriszyp marked this pull request as draft October 6, 2026 17:56
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

get-sync.bench.ts

getSync() > random keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 24.48K ops/sec 40.84 39.45 1,992.929 0.141 122,424
🥈 rocksdb 2 10.42K ops/sec 96.01 92.44 23,841.715 0.962 52,076

getSync() > sequential keys - small key size (100 records)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 28.41K ops/sec 35.20 34.15 605.421 0.105 142,039
🥈 rocksdb 2 9.53K ops/sec 104.974 101.304 3,653.076 0.150 47,631

ranges.bench.ts

getRange() > small range (100 records, 50 range)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 lmdb 1 23.77K ops/sec 42.08 37.04 1,874.665 0.286 118,831
🥈 rocksdb 2 15.33K ops/sec 65.22 56.72 1,070.235 0.121 76,665

realistic-load.bench.ts

Realistic write load with workers > write variable records with transaction log

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 461.38 ops/sec 2,167.425 44.28 68,795.19 15.82 923
🥈 lmdb 2 25.91 ops/sec 38,590.428 441.313 1,206,872.942 136.277 64.00

transaction-log.bench.ts

Transaction log > read 100 iterators while write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 34.69K ops/sec 28.82 13.76 20,840.486 0.845 173,465
🥈 lmdb 2 436.75 ops/sec 2,289.641 127.27 13,709.366 1.31 2,184

Transaction log > read one entry from random position from log with 1000 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 602.63K ops/sec 1.66 1.41 5,068.653 0.210 3,013,140
🥈 lmdb 2 482.69K ops/sec 2.07 1.18 8,478.029 0.552 2,413,474

worker-put-sync.bench.ts

putSync() > random keys - small key size (100 records, 10 workers)

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 842.38 ops/sec 1,187.118 1,051.391 4,046.753 0.391 1,685
🥈 lmdb 2 1.14 ops/sec 877,483.44 818,499.958 987,701.877 3.72 10.00

worker-transaction-log.bench.ts

Transaction log with workers > write log with 100 byte records

Implementation Rank Operations/sec Mean (ms) Min (ms) Max (ms) RME (%) Samples
🥇 rocksdb 1 21.14K ops/sec 47.31 30.04 20,580.291 2.08 42,272
🥈 lmdb 2 830.47 ops/sec 1,204.143 181.512 20,995.98 5.64 1,661

Results from commit d7f09a8

@kriszyp
kriszyp marked this pull request as ready for review October 6, 2026 19:25
Comment thread README.md
## Development

This package requires Node.js 18 or higher, pnpm, and a C++ compiler.
This package requires Node.js `^22.18.0 || >=24.0.0`, pnpm, and a C++ compiler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch!

@kriszyp
kriszyp merged commit 8c73216 into main Oct 6, 2026
45 of 47 checks passed
@kriszyp
kriszyp deleted the fix/create-rocksdb-error-return-value branch October 6, 2026 22:35
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