loud-rejection
loud-rejection installs process listeners that track promise rejections nobody handled, and if any are still unhandled when the process is about to exit, it prints their stacks to stderr and exits with code 1. It exists because Node used to swallow those rejections entirely: a forgotten catch meant a failed operation and a zero exit code. Two ways to use it, a function call or a side-effect import of loud-rejection/register. Its own README now carries the conclusion in bold: from Node 15 onward the default behaviour already throws on unhandled rejections, which makes this package moot.
A useful patch for a real Node 8 to 14 problem that the runtime fixed in Node 15, and its own README says as much. Delete it when you drop Node 14, and do not add it to new projects unless you have deliberately disabled Node's own unhandled-rejection handling.
Use it if
- You are stuck supporting Node 8 through 14, where an unhandled rejection prints a warning and lets the process exit zero, and you want a non-zero exit instead
- You are reading an old CLI or test runner that imports loud-rejection/register and you need to know what that line is doing before deleting it
- You deliberately run with --unhandled-rejections=warn or =none and still want a hard failure at exit rather than at the moment of rejection
- You want rejections collected and reported together at process end rather than the first one killing the process immediately, which is what Node's default does
- You are on Node 15 or newer, where an unhandled rejection already throws and exits 1 with no package at all; verified on Node 22, where the process dies before a 300 ms timer fires
- You want to fail fast, because this package waits until exit to report, so a rejected promise can leave your program running in a broken state for the rest of its life
- You are writing a library rather than an application: the README says so directly, and installing global process handlers from a dependency changes behaviour for whoever consumes you
- You want it maintained: the last publish was 2.2.0 in September 2019 and the last repository push was January 2021, on a package whose own docs declare it obsolete
- You need ESM or TypeScript types, since 2.2.0 is CommonJS with no exports map and no bundled declarations
- You are only adding it to satisfy a lint rule or a habit, in which case you are shipping two transitive dependencies, currently-unhandled and signal-exit, for behaviour the runtime already has
Setup reality
There is barely any setup, which is not the problem. npm install loud-rejection pulls currently-unhandled and signal-exit, then you either call the exported function once at the top of your entry file or write require('loud-rejection/register') and let the side effect do it. The engines field says node >= 8. Everything difficult about this package is deciding whether you should have it at all. The behaviour it adds and the behaviour Node now has are not the same thing, and the difference matters. Node 15 and later throw an ERR_UNHANDLED_REJECTION at the moment a rejection is determined to be unhandled, which stops the process right there; a quick check on Node 22 shows the program dying before a timer scheduled 300 ms later ever runs. loud-rejection instead accumulates rejections and only reports them from a signal-exit hook as the process winds down, so your program keeps executing on top of a failure it already knows about. If you install this on a modern Node you now have two mechanisms competing, and the runtime's wins because it fires first. That is why the useful configuration is narrow: it only makes sense paired with --unhandled-rejections=warn or =none, where you have deliberately turned the runtime behaviour off. The other trap is scope. Calling it from library code installs process-level listeners on behalf of an application that never asked, which is exactly what the README warns against. If what you actually want is fail-fast, hard-rejection from the same author does that, and on current Node so does doing nothing.
Patterns
Turn it on at the top of an entry fileinstall-listeners
const loudRejection = require('loud-rejection')
loudRejection()
main().catch((err) => {
console.error(err)
process.exit(1)
})Call it once, in the application entry point only. Calling it from a module you publish installs process handlers on behalf of someone else's program.
Use the register entry pointregister-side-effect
// CommonJS
require('loud-rejection/register')
// with an ESM-to-CJS build step
import 'loud-rejection/register'Same effect as calling the function, just import-friendly. 2.2.0 has no exports map, so native ESM resolution depends on your bundler or Node's CJS interop.
Send the report somewhere other than stderrcustom-logger
const loudRejection = require('loud-rejection')
loudRejection((stack) => {
logger.error({ event: 'unhandled_rejection', stack })
})The callback receives the error stack as a string and replaces console.error. The exit code is still set to 1 for you; the callback does not control it.
Confirm the runtime already does thischeck-node-default
// node check.js (Node 22)
Process = process
process.on('exit', (code) => console.log('exit code:', code))
Promise.reject(new Error('boom'))
setTimeout(() => console.log('never printed'), 300)
// exit code: 1
// throws ERR_UNHANDLED_REJECTION before the timer runsRun this before installing the package. On Node 15 and later the timer never fires, which is stricter than what loud-rejection gives you.
The one configuration where it still adds somethingpair-with-warn-mode
// package.json
{
"scripts": {
"start": "node --unhandled-rejections=warn -r loud-rejection/register app.js"
}
}With the runtime set to warn, nothing stops the process mid-run, and loud-rejection turns the accumulated warnings into a non-zero exit at the end.
Swap to hard-rejection when you want an immediate crashfail-fast-instead
-require('loud-rejection/register')
+require('hard-rejection/register')
// hard-rejection throws at the moment of the unhandled rejection,
// matching what Node 15+ does by defaultSame author, opposite timing. On Node 15 and later this is also redundant, but it at least matches the runtime's semantics instead of contradicting them.
Observe rejections without changing the exit codetrack-without-exiting
const currentlyUnhandled = require('currently-unhandled')()
setInterval(() => {
const pending = currentlyUnhandled()
if (pending.length) metrics.gauge('unhandled_rejections', pending.length)
}, 5000)This is the dependency loud-rejection builds on. Use it directly when you want telemetry and want the process lifecycle left alone.
Get the behaviour from a flag rather than a dependencycli-flag-alternative
node --unhandled-rejections=strict app.js
// or, without touching the command line
// package.json -> "start": "NODE_OPTIONS=--unhandled-rejections=strict node app.js"strict is the default from Node 15 onward, so the flag is only needed if something in your stack set it to warn or none.
Drop it when the Node floor moves past 14remove-on-upgrade
// package.json
{
"engines": { "node": ">=20" },
"dependencies": {
- "loud-rejection": "^2.2.0"
}
}
-require('loud-rejection/register')Removing it makes failures stricter, not looser. Run the test suite afterwards, because rejections that used to surface only at exit will now stop the process where they happen.
Fix the cause rather than the symptomhandle-rejection-properly
// leaks a rejection
readConfig()
// does not
readConfig().catch(onFatal)
// or
try {
await readConfig()
} catch (err) {
onFatal(err)
}loud-rejection only tells you a catch is missing. The lint rules that find these statically, such as no-floating-promises, prevent the class of bug instead.
Keep global handlers out of published packagesavoid-in-libraries
// bad: src/index.js of a published library
require('loud-rejection/register')
// fine: bin/cli.js of the same repo, an application entry point
require('loud-rejection/register')
require('../src/cli').run(process.argv.slice(2))The README states this rule outright. A library that installs process listeners changes the exit behaviour of every application that depends on it.
Find where a rejection lost its handlertrace-missing-stack
// node -r trace-unhandled/register app.js
Promise.reject(new Error('boom'))
// prints the promise chain and creation site,
// not just the error stackUseful when the reported stack points into library internals. Development only: the tracing has real overhead and is not something to ship.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| hard-rejection | npm | You want the process to die at the moment of the unhandled rejection instead of waiting until exit, on Node versions older than 15 |
| currently-unhandled | npm | You want the list of currently unhandled rejections to inspect or report yourself, without any package deciding your exit code |
| trace-unhandled | npm | The stack you get is useless and you need to find which promise chain dropped the rejection in the first place |