fix(runtime): keep a rejecting fire-and-forget verb from killing the script#34
Merged
ralyodio merged 1 commit intoJul 25, 2026
Conversation
…script
An async verb the script did not await had its raw promise pushed onto
`pending`, and nothing subscribed to it until the `Promise.allSettled`
drain after the script body finished. If the script crossed a tick
boundary in between (the documented `notify("..."); await ask("...")`
pattern does exactly that), Node saw an unhandled rejection first and
tore the process down, so the drain never ran, the awaited verb was
killed mid-flight, and the user got a raw stack trace instead of the
clean one-line error bin/moshcode.mjs prints.
Queue `Promise.allSettled([result])` instead. It subscribes immediately,
so the rejection is observed from the moment it is queued. The script
still receives the original promise, so an awaited call fails exactly as
before, and the drain keeps swallowing fire-and-forget failures as it
already did.
Regression tests cover both halves: an un-awaited rejecting verb no
longer stops a script that keeps running, and an awaited one still
propagates.
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.
The bug
makeScopepushes the raw promise from an un-awaited async verb ontopending, and nothing subscribes to it until thePromise.allSettled(pending)drain runs after the script body finishes (src/runtime.mjs:67and:139).If the script crosses a tick boundary between the fire-and-forget call and the end of the body, Node flags the rejection as unhandled and tears the process down before the drain can ever observe it. The README's own
notify(...)thenawait ask(...)pattern does exactly that.The user cannot catch this: a
try/catcharound an un-awaited call does not help, and it bypasses the cleantry/catchinbin/moshcode.mjs, so they get a raw stack trace instead of the one-line error.Real trigger in tree:
src/notify.mjs:44does an unguardedawait res.json(), so any 200 with an empty or non-JSON body (a proxy hiccup) makesnotify()reject.Reproduction
Before:
after waitnever prints. After the fix it prints andrunScriptreturns{ iterations: 0, stopped: false }, exit 0.The fix
Queue
Promise.allSettled([result])rather than the bareresult. The wrapper subscribes synchronously, so the rejection is observed the moment it is queued and can never be reported as unhandled.Deliberately minimal, and semantics are unchanged:
allSettled, so fire-and-forget failures stay swallowed, matching the comment's stated intent.Tests
Two regression tests in
test/runtime.test.mjs, one per half of the contract:main, passes with the fix);Full suite: 136 pass before, 138 pass / 0 fail after (
npm test).Separate hardening worth doing, not included here to keep this to one change: wrapping
res.json()insrc/notify.mjs:44so a bad body surfaces as{ ok: false, error }like the other failure modes.