Async Era
ES2017+2017 – 2019`async`/`await` made the microtask queue an everyday concern. Asynchronous code finally reads top-to-bottom while still yielding to the loop at each await.
A rejected promise with no `.catch` or `await` triggers `unhandledrejection`. The timing is subtle: a handler attached in the *same task* still saves it — the report only fires after the microtask queue drains with the promise still unhandled.
// Rescued within the same task: no unhandledrejection fires.
Promise.reject(new Error("rescued in same task"))
.catch((err) => console.log("caught:", err.message));
// Handler attached in a later macrotask — too late to prevent the report.
const late = Promise.reject(new Error("late handler"));
setTimeout(() => late.catch(() => console.log("late rescue ran")), 0);
// Never handled at all — reported as an uncaught (in promise) error.
Promise.reject(new Error("never handled"));
console.log("sync done");| V8 | The engine only fires the host's `unhandledrejection` mechanism after the microtask queue drains — a handler attached in the same task still rescues it. |
|---|---|
| Node.js | Since Node 15 the default `--unhandled-rejections=throw` makes an unhandled rejection crash the process; `warn`, `strict`, and `none` are the other modes. |
| Deno | Prompts on the first unhandled rejection so you can inspect, ignore, or exit — use `--no-prompt` to fail fast in CI. |
| Bun | Prints `Uncaught (in promise) …` and exits with a non-zero status by default, like Node in strict mode. |