Skip to content

Forward error causes across the WASM worker boundary (0.10.1) - #17

Merged
andrekuehne merged 1 commit into
masterfrom
ccr-d384753d-r9iwvt
Oct 22, 2026
Merged

andrekuehne merged 1 commit into
masterfrom
ccr-d384753d-r9iwvt

Conversation

@andrekuehne

Copy link
Copy Markdown
Owner

Problem

Worker-backed models (createNecWorkerModel, createNecArraySolver) wrap a WASM trap as NecRuntimeError("<op> failed at the WASM boundary", { cause }). The worker protocol dropped that cause: serializeError sent only name/code/message/details, and reviveError and the client's crash path rebuilt errors with details alone. Callers saw "solve failed at the WASM boundary" and never the reason, such as "memory access out of bounds" or "Aborted()".

Change

  • serializeError now emits an optional cause: { name, message }, built from the error's cause. The object is plain strings, so it survives structured clone. A non-Error cause uses String(cause) as its message. A plain Error keeps its own cause too.
  • reviveError and the worker-client crash path rebuild the cause as an Error with the same name and message. Both go through the new reviveErrorOptions.
  • The wire format stays backward compatible. Workers that omit the field still revive, with cause === undefined.
  • NecStateError accepts an optional fourth cause argument, so revived state errors keep both their operation/state and their cause.
  • The package moves to 0.10.1, with a CHANGELOG entry and a doc note in docs/wasm-api.md.

Tests

  • A NecRuntimeError wrapping a real WebAssembly.RuntimeError round-trips through serializeError, structuredClone and reviveError. The tests use a tiny module whose exported function runs unreachable. The revived error keeps cause.name === "RuntimeError", the original message and its details.
  • Errors without a cause, and legacy wire errors, revive with cause === undefined.
  • A string cause survives as an Error. A NecStateError keeps operation/state plus its cause. A plain Error keeps its own cause.
  • End to end through createNecWorkerModelFromHost over a real MessageChannel, a failure thrown in the worker and a crash message both reach the caller with their cause.

Locally, typecheck passes, as do the worker-protocol and worker-client tests (27/27, 5 repeated runs). The suites that need the Emscripten build couldn't run here because the generated WASM is missing; they fail the same way on unmodified master. CI runs them.

Release (not done in this PR)

After merge, pushing tag wasm-v0.10.1 on the merge commit publishes the tested tarball to npm with --provenance. The release job in build.yml checks that the tag matches package.json. The tag is left for the maintainer.


Generated by Claude Code

The worker protocol serialized only name/code/message/details, so a
NecRuntimeError("<op> failed at the WASM boundary") from a worker-backed
model lost its cause (e.g. "memory access out of bounds", "Aborted()").

serializeError now emits an optional, structured-clone-safe
cause: { name, message }; reviveError and the client's crash path
rebuild it as an Error with the same name and message. Older workers
that omit the field still revive with cause === undefined.
NecStateError accepts an optional cause argument so state errors keep
theirs too.

Bump @necpp-engine/wasm to 0.10.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyKgo3fEVyzmSVf2CVaERy
@andrekuehne
andrekuehne merged commit 713285e into master Oct 22, 2026
12 checks passed
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