Repository navigation
be-js: mark a PromiseBuilder's promise handled when it is made - #602
Open
notactuallytreyanastasio wants to merge 1 commit into
Open
notactuallytreyanastasio wants to merge 1 commit into
notactuallytreyanastasio wants to merge 1 commit into
Conversation
Node ends the process when a promise is rejected and has no handler by
the time the microtask queue drains. In generated js, `runAsync` starts
an async block from a `setTimeout`, and the block's `await` attaches its
`then` only once the generator reaches it. So a promise broken from
top-level code before the block runs has no handler yet:
let b = new PromiseBuilder<Int>();
async { (): GeneratorResult<Empty> extends GeneratorFn =>
let v = await b.promise orelse -1;
console.log("got ${v}");
}
b.breakPromise();
Before, on js:
UnhandledPromiseRejection: This error originated either by throwing
inside of an async function without a catch block, or by rejecting a
promise which was not handled with .catch(). The promise rejected with
the reason "undefined".
code: 'ERR_UNHANDLED_REJECTION'
Run failed
After, as on py, java, rust and the interpreter:
got -1
The PromiseBuilder constructor now attaches a no-op rejection handler to
its promise. That only tells node the rejection is handled. An `await`
that arrives later still adds its own `then` and gets the rejection, so
the `orelse` runs. A broken promise that nothing ever awaits is silent,
which is what the other backends do with one.
Starting the generator synchronously in `runAsync` would also attach the
handler in time, but it would run the block's first steps before the
rest of top-level code, an ordering no other backend has.
The new functional test control-flow/async-broken-early fails on js
before this change and passes on js, py, java 17, rust and the
interpreter after it. It is marked for lua under temperlang#144 like
control-flow/async, and for cpp under temperlang#520: cpp prints nothing for it,
since it never resumes a block whose promise top-level code settles.
Fixes temperlang#551.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Bobby Grayson <bobbbygrayson+github@gmail.com>
mikesamuel
approved these changes
Oct 12, 2026
Contributor
|
Ok, so "or by rejecting a promise which was not handled with .catch()" is just some spam that the runtime prints based on a heuristic of which promises are probably being misused. |
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.
Node ends the process when a promise is rejected and has no handler by
the time the microtask queue drains. In generated js,
runAsyncstartsan async block from a
setTimeout, and the block'sawaitattaches itsthenonly once the generator reaches it. So a promise broken fromtop-level code before the block runs has no handler yet:
Before, on js:
After, as on py, java, rust and the interpreter:
The PromiseBuilder constructor now attaches a no-op rejection handler to
its promise. That only tells node the rejection is handled. An
awaitthat arrives later still adds its own
thenand gets the rejection, sothe
orelseruns. A broken promise that nothing ever awaits is silent,which is what the other backends do with one.
Starting the generator synchronously in
runAsyncwould also attach thehandler in time, but it would run the block's first steps before the
rest of top-level code, an ordering no other backend has.
The new functional test control-flow/async-broken-early fails on js
before this change and passes on js, py, java 17, rust and the
interpreter after it. It is marked for lua under #144 like
control-flow/async, and for cpp under #520: cpp prints nothing for it,
since it never resumes a block whose promise top-level code settles.
Fixes #551.
Repro case: https://github.com/notactuallytreyanastasio/temper-issue-repros/tree/main/29-js-broken-promise-orelse
🤖 Generated with Claude Code