How does a program observe unhandled promise rejections globally in a browser versus in Node, and what does each runtime do by default when nothing handles a rejection?
answer
- one language rule, two host reactions
- the browser fires a cancellable event
- preventDefault only silences the console
- Node capitalises the event differently
- a listener replaces the crash policy
basics
~20 sBrowsers fire an unhandledrejection event on the global object, carrying .promise and .reason, and log the error unless you call preventDefault(). Node emits process 'unhandledRejection' and, if no listener exists, raises it as an uncaught exception and exits non-zero.
solid answer
~40 sIn a browser you listen with `addEventListener('unhandledrejection', handler)`. The event is a `PromiseRejectionEvent` exposing `event.promise` and `event.reason`; the default action is to report the error to the console, and `event.preventDefault()` suppresses that. A matching `rejectionhandled` event fires if a handler shows up afterwards. In Node the equivalent is `process.on('unhandledRejection', (reason, promise) => ...)`, with `process.on('rejectionHandled', ...)` as its counterpart. The important difference is the default: since Node 15 an unhandled rejection is raised as an uncaught exception, printing it and terminating the process with a non-zero exit code — but only when no `unhandledRejection` listener is registered. Registering one replaces that behaviour entirely, which is why a careless global hook can turn a crash into silence. The `--unhandled-rejections` flag selects the policy explicitly.
code
javascript · 10 lines// Browser: DOM event with .promise and .reason
addEventListener('unhandledrejection', (event) => {
const reason = event.reason instanceof Error
? event.reason.message
: String(event.reason);
console.warn('reporting:', reason);
event.preventDefault(); // no duplicate console error
});
Promise.reject(new Error('boom'));go deeper
Know that both hosts give you a global notification: unhandledrejection on the browser's global object and unhandledRejection on Node's process. Remember the browser only logs while Node exits by default.
Explain the payload difference — a PromiseRejectionEvent with .promise/.reason versus (reason, promise) arguments — and that Node's default throw mode only applies when no listener is registered.
Show how you wire the hook into real reporting: normalising non-Error reasons, avoiding duplicate console noise, and keeping a Node hook fail-fast by setting a non-zero exit code and shutting down cleanly.
Own the policy question across services — whether the global hook is a bug detector or a safety net, what belongs at the call site instead, and how you stop a well-meant logging hook from quietly disabling fail-fast behaviour fleet-wide.
## Two hosts, one language mechanism ECMAScript itself only defines that the host must be *notified* when a promise is rejected with no handler, and again if a handler appears later. Everything observable — the event name, the payload, whether the process dies — is the host's business. That is why the browser and Node answers differ, and why an interviewer asking this is really checking whether you know which layer owns what. ## Browser ```js addEventListener('unhandledrejection', (event) => { reportToBackend({ reason: event.reason, source: 'unhandledrejection' }); event.preventDefault(); // suppress the default console report }); addEventListener('rejectionhandled', (event) => { retractReport(event.promise); }); ``` The object passed in is a `PromiseRejectionEvent` with two extra properties: `promise`, the promise that rejected, and `reason`, the rejection value. The default action of the event is to report the error the way an uncaught exception is reported — a console entry. Calling `preventDefault()` cancels that, which is what you do when you have your own reporter and do not want duplicate noise. Crucially, a browser does **not** stop your page. The rest of the script keeps running; you simply have an error nobody handled and possibly a UI stuck in a loading state. `rejectionhandled` fires when a handler is attached to a promise that was already reported. It exists so a reporter can retract; it does not undo anything else. Because `reason` is whatever was thrown, it is not necessarily an `Error` — a rejection can carry a string, `undefined`, or any value at all. A global reporter should normalise defensively rather than reading `reason.stack` blindly. ## Node ```js process.on('unhandledRejection', (reason, promise) => { log.fatal({ reason }, 'unhandled rejection'); process.exitCode = 1; shutdownGracefully(); }); process.on('rejectionHandled', (promise) => { log.warn('a previously unhandled rejection got a handler'); }); ``` Note the shape difference: it is a Node `EventEmitter` event with two *arguments*, `(reason, promise)`, not a DOM event object. The capitalisation also differs — Node uses camelCase `unhandledRejection`, the browser uses all-lowercase `unhandledrejection`. Getting that wrong silently registers a listener that never fires, which is a fun way to lose every error report in a service. The behavioural difference is the one that matters most. Node's default mode is `throw`: if there is no `unhandledRejection` listener, the rejection is raised as an uncaught exception, printed, and the process exits with a non-zero code. Node exposes the policy through `--unhandled-rejections=<mode>`, whose modes include `throw` (the default), `strict`, `warn`, `warn-with-error-code`, and `none`. Before Node 15 the default was merely a warning, which is why so much older material claims a rejection "just logs". ## The trap: a listener silently disables the crash Because the `throw` policy only applies when no listener is registered, adding ```js process.on('unhandledRejection', (reason) => console.error(reason)); ``` changes your service from fail-fast to fail-quietly. Every future unhandled rejection now prints a line and the process continues in whatever half-finished state produced it. If you register a hook, decide deliberately whether it ends with a shutdown; a hook that only logs is a policy change disguised as observability. ## What a global hook is and is not for It is a **detector of bugs**, not an error-handling strategy. Anything you actually expect to fail — a network call, a parse, a write — should be handled at its call site, where you have the context to retry, fall back, or surface a message to the user. By the time a rejection reaches the global hook you have lost that context: you often cannot tell which request it belonged to, and you cannot resume anything. So the productive use is: 1. Wire it to your error reporter with as much context as you can attach. 2. Treat every entry as a defect to fix, not as normal operation. 3. In Node, be explicit about whether the process should survive. A useful signal of maturity in the answer is noting that the browser's `unhandledrejection` covers async failures the way the older `error`/`window.onerror` path covers synchronous ones — a client-side reporter needs both, or it will miss every promise-shaped bug in the application.
- Why can registering process.on('unhandledRejection') make a service less safe?Because Node's crash-by-default only applies when no such listener exists. Registering one replaces the `throw` policy, so a hook that merely logs converts every unhandled rejection into a silent continuation in an unknown state. If you register a hook, decide explicitly whether it ends in a shutdown, and set a non-zero exit code when it does.
- Does the browser's unhandledrejection event stop the page from running?No. The default action is only to report the error the way an uncaught exception is reported — a console entry — and `event.preventDefault()` suppresses even that. Script execution continues, which is why the visible symptom is usually a stuck UI rather than a crash, and why client-side reporters must listen for this event explicitly.
- Is event.reason always an Error object?No. A promise can be rejected with any value — a string, a number, `undefined`, a plain object — because `Promise.reject` and `throw` accept anything. A global reporter that reads `reason.stack` or `reason.message` unconditionally will produce useless or crashing reports, so normalise the value before logging it.
saying these in an interview costs you the question
- Uses the browser's lowercase event name on Node's process
- Thinks preventDefault stops the rejection rather than the log
- Assumes the browser terminates the page on unhandled rejection
- Believes a logging-only Node hook preserves crash-on-rejection
- Treats the global hook as the app's error-handling strategy