mrkeyoor.com_
Sat 08 Aug 22:53 UTC
npmUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability5/5The surface is one function that optionally takes a log callback, plus a register entry point that calls it for you. That has been the whole API since 1.x; the 2.0.0 major in March 2019 raised the Node floor to 8 rather than changing how you call it. Nothing has moved since 2.2.0 in September 2019 and nothing is going to, because the project considers itself finished. Code written against it a decade ago still runs unchanged.
Docs4/5The README is short and unusually honest for a package with millions of weekly installs. It shows both usage forms, documents the single log option, states plainly that this belongs in applications and not in reusable modules, and puts the obsolescence notice in bold near the top rather than burying it. What it does not explain is how the exit-time reporting interacts with Node's own immediate throw, which is the question anyone installing it today actually needs answered.
Maintenance2/5The repository is not archived and has zero open issues, but the last push was January 2021 and the last npm publish was 2.2.0 in September 2019. Its two dependencies are equally still: currently-unhandled at 0.4.x and signal-exit at 3.x. This is a deliberate end state rather than neglect, since the maintainer documented that the runtime made the package unnecessary. Either way, nothing new will land, and 4.4 million weekly downloads are mostly old dependency trees rather than new adopters.
Ecosystem3/5About 4.4 million weekly downloads, driven largely by transitive use in CLI scaffolding and older test tooling rather than by direct installs. There is a small family around it from the same author, hard-rejection for fail-fast and currently-unhandled for the underlying tracking, so swapping between them is trivial. Beyond that family there is nothing to integrate with: no plugins, no framework hooks, no TypeScript declarations, and no browser story, since browsers already log unhandled rejections to the console.

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
Skip it if

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 runs

Run 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 default

Same 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 stack

Useful when the reported stack points into library internals. Development only: the tracing has real overhead and is not something to ship.

Alternatives

PackageRegistryPick it when
hard-rejectionnpmYou want the process to die at the moment of the unhandled rejection instead of waiting until exit, on Node versions older than 15
currently-unhandlednpmYou want the list of currently unhandled rejections to inspect or report yourself, without any package deciding your exit code
trace-unhandlednpmThe stack you get is useless and you need to find which promise chain dropped the rejection in the first place