Skip to content

be-js: mark a PromiseBuilder's promise handled when it is made - #602

Open
notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/js-broken-promise-handled
Open

notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/js-broken-promise-handled

Conversation

@notactuallytreyanastasio

Copy link
Copy Markdown
Contributor

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 #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

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

Copy link
Copy Markdown
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.

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.

js: a promise broken before its async block awaits it kills node with UnhandledPromiseRejection, though the await has an orelse

2 participants