Repository navigation
Fix N-API error builders returning an unwritten error value - #906
Merged
Merged
Conversation
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
Contributor
There was a problem hiding this comment.
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
marked this pull request as ready for review
October 6, 2026 17:38
kriszyp
force-pushed
the
fix/create-rocksdb-error-return-value
branch
from
October 6, 2026 17:56
2406269 to
bd9ff25
Compare
kriszyp
marked this pull request as draft
October 6, 2026 17:56
Contributor
📊 Benchmark Resultsget-sync.bench.tsgetSync() > random keys - small key size (100 records)
getSync() > sequential keys - small key size (100 records)
ranges.bench.tsgetRange() > small range (100 records, 50 range)
realistic-load.bench.tsRealistic write load with workers > write variable records with transaction log
transaction-log.bench.tsTransaction log > read 100 iterators while write log with 100 byte records
Transaction log > read one entry from random position from log with 1000 100 byte records
worker-put-sync.bench.tsputSync() > random keys - small key size (100 records, 10 workers)
worker-transaction-log.bench.tsTransaction log with workers > write log with 100 byte records
Results from commit d7f09a8 |
kriszyp
marked this pull request as ready for review
October 6, 2026 19:25
cb1kenobi
approved these changes
Oct 6, 2026
| ## 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
createRocksDBErrorandcreateJSError(src/binding/napi/helpers.cpp) return their result through anerrorout-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 reachednapi_throwor a promiserejectunwritten. The async completion sites (backup, backup stream, checkpoint, commit) would then never settle their promise.💡 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.takeFailedCallExceptionchecksnapi_is_exception_pendingbefore it synthesizes, and reads the extended error first, because the pending check resets it.createRocksDBErrorandcreateJSErrorgoes throughNAPI_STATUS_TAKES_ERROR.Invariant: after either builder returns,
erroris a validnapi_value. The contract is insrc/binding/napi/DESIGN.md.⚖️ Alternatives
napi_valueand 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.napi_throwfallback, 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
src/binding/napi/helpers.cpp, recovery above. Header signatures unchanged.test/fixtures/fork-error-object-failure.mts. Runs in a child process, patchesObject.create(the global the builders call), and checks: a thrown value comes back as the same value (asyncbackups.list), a non-callable factory yields anError(synctransactionSyncwithsetTimestamp(-1), and async).test/error-object-failure.test.ts. Kills the child at 10 s, waits for it to exit, then removes the database directory.src/binding/napi/DESIGN.mdand its index entry inDESIGN.md. Records which regression case discriminates (async only; see Verification).^22.18.0 || >=24.0.0, matchingpackage.jsonengines.✅ Verification
pnpm build(bundle + native binding): passes; no compiler warnings inhelpers.cpp.pnpm check(type-check, lint, format): passes.pnpm teston the rebased headbd9ff25f: 78 files passed, 1 skipped; 1152 tests passed, 10 skipped. The branch was rebased ontomaina6260366after Make optimistic commit lock buckets and validation policy configurable #897, Stop verification-table slot collisions from answering FRESH for another key #901, Run a database's async commits on up to four concurrent commit threads #902 and Read every entry of a transaction-log segment that one transaction pushed past transactionLogMaxSize #890 merged; the only conflict was theDESIGN.mdindex, resolved by keeping both entries.src/binding/napi/helpers.cpptaken from1b165e09, the fixture exits non-zero (the thrown-value case's reject never runs, because the builder's pending exception blocks it). It passes on this branch.createJSErrorcase passes on the pre-fix builder too (the synthesized exception is already pending, so the caller'snapi_throwis a no-op), so it checks the new path's result, not the regression.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.prototypelookup, and the now-deaderror == nullptrguards indatabase.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-1Review-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