Skip to content

[v8-bridge] surface unhandled promise rejections via cs_v8_last_error#634

Merged
cs01 merged 1 commit intomainfrom
fix/issue-617-promise-reject
Apr 21, 2026
Merged

[v8-bridge] surface unhandled promise rejections via cs_v8_last_error#634
cs01 merged 1 commit intomainfrom
fix/issue-617-promise-reject

Conversation

@cs01
Copy link
Copy Markdown
Owner

@cs01 cs01 commented Apr 21, 2026

Before

Unhandled promise rejections in @chadscript: interpret pragma code vanished silently. A script like new Promise((_, rej) => rej("boom")) with no .catch() would run, SpinEventLoop would drain, and cs_v8_last_error stayed empty — the caller treated the script as successful.

After

V8's PromiseRejectCallback is wired on isolate init. Unhandled rejections (event kPromiseRejectWithNoHandler — specifically ignoring the kPromiseHandlerAddedAfterReject false-positive) are captured into a thread-local. After SpinEventLoop returns, both run_script and cs_v8_eval_script_node propagate the captured rejection into t_last_error and fail the call so cs_v8_last_error reads it like any synchronous throw.

Description

~49 LOC, all in c_bridges/node-bridge.cc:

  • New thread_local t_last_unhandled_rejection separate from t_last_error so a sync throw isn't clobbered.
  • SetPromiseRejectCallback installed in cs_node_lazy_init right after the Environment is set up.
  • Callback filters on kPromiseRejectWithNoHandler, stringifies the rejection value.
  • Cleared at the start of each eval; propagated + cleared after each eval's SpinEventLoop.

Follows the same per-call-clear → run → check-after pattern as existing try_catch error handling.

Test plan

  • Reads compile cleanly (visual review against existing patterns in node-bridge.cc).
  • Runtime verification requires a built vendor/node/ + libnode toolchain. My worktree doesn't have it; node-bridge.cc isn't compiled in CI either (no libnode step in ci.yml). Anyone with libnode locally can run a pragma file that rejects without .catch and observe that cs_v8_last_error now returns the rejection string instead of empty.
  • Follow-up test: a fixture under tests/fixtures/pragma-interpret/ that asserts a pragma file with an unhandled rejection fails with the expected message (once pragma test infra exists).

Closes #617.

Next steps / Gaps

  • Dedicated fixture under a new pragma-interpret/ test category once the pragma+libnode path has a CI lane.
  • kPromiseHandlerAddedAfterReject is ignored so a late .catch() doesn't trip the error — but Node's process-level unhandledRejection event is NOT forwarded; if a Node handler is attached, V8 still fires the kPromiseRejectWithNoHandler event before the handler sees it. Cleaner integration would route through process.on('unhandledRejection') instead.
  • Multiple rejections in a single eval are collapsed into one — only the last is reported.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results (Linux x86-64)

Benchmark C ChadScript Go Node Place
Cold Start 0.9ms 0.8ms 1.2ms 27.0ms 🥇
Fibonacci 0.814s 0.763s 1.560s 3.216s 🥇
Hash Map Lookup 0.091s 0.063s 0.090s 0.113s 🥇
Binary Trees 1.375s 1.271s 2.709s 1.181s 🥈
File I/O 0.122s 0.093s 0.087s 0.208s 🥈
JSON Parse/Stringify 0.035s 0.053s 0.182s 0.135s 🥈
N-Body Simulation 1.698s 2.131s 2.202s 2.385s 🥈
Regex Match 0.016s 0.005s 0.021s 0.005s 🥈
SQLite 0.052s 0.374s 0.503s 0.417s 🥈
Monte Carlo Pi 0.389s 0.410s 0.405s 2.248s 🥉
Quicksort 0.215s 0.246s 0.213s 0.262s 🥉
Sieve of Eratosthenes 0.016s 0.028s 0.018s 0.039s 🥉
String Manipulation 0.008s 0.018s 0.017s 0.036s 🥉
Matrix Multiply 0.520s 0.729s 0.566s 0.358s #4

CLI Tool Benchmarks

Benchmark ChadScript grep node xxd Place
Hex Dump 0.564s 1.118s 0.131s 🥈
Recursive Grep 0.020s 0.010s 0.103s 🥈

@cs01 cs01 merged commit d003eab into main Apr 21, 2026
15 checks passed
@cs01 cs01 deleted the fix/issue-617-promise-reject branch April 21, 2026 13:11
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.

v8 boundary: unhandled promise rejection hook

1 participant