Skip to content
Latchkey

Node UnhandledPromiseRejection Crashes the Job in CI - Handle the Rejection

On modern Node the default unhandled rejection mode is throw, so a promise that rejects with no catch terminates the process and fails the CI job.

What this error means

A node process prints UnhandledPromiseRejection (or an unhandled rejection stack) and exits non-zero. The rejecting promise was never awaited or caught.

node
node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^
[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().

Diagnose it: the shell in CI is not your shell

Package scripts run under a different shell, a different PATH, and a non-interactive environment on a runner. Most scripts that fail only in CI are relying on something the login shell gave them locally: a tool on PATH, an environment variable from a dotfile, or a TTY.

Terminal
# what the script can actually see
npm run env | grep -E "^(PATH|NODE_ENV|CI)=" 

# is the binary on PATH for the script, not just for you?
npm exec -- which <tool> || echo "not resolvable from npm scripts"

# run the exact script with tracing
sh -x -c "$(node -p "require('./package.json').scripts.build")"

Common causes

A promise without await or catch

An async call is fired and forgotten; when it rejects there is no handler, so the default throw mode kills the process.

A missing await inside an async function

A rejection bubbles out of an un-awaited inner promise instead of being caught by the surrounding try/catch.

How to fix it

Await and wrap in try/catch

  1. Await the promise so its rejection is observable.
  2. Wrap the await in try/catch to handle the failure.
JavaScript
try {
  await doWork();
} catch (err) {
  console.error(err);
  process.exitCode = 1;
}

Add a global handler as a backstop

  1. Register an unhandledRejection listener to log and exit deliberately.
  2. Treat it as a safety net, not a substitute for local handling.
JavaScript
process.on('unhandledRejection', (err) => {
  console.error(err);
  process.exit(1);
});

Make failures fail the job

A multi-command script can report success while a middle command failed, which produces the worst kind of CI result: a green build that shipped something broken.

.github/workflows/ci.yml
# pipefail is NOT set by default in every runner shell
- name: Build
  shell: bash
  run: |
    set -euo pipefail
    npm run build | tee build.log

How to prevent it

  • Enable the no-floating-promises lint rule, await every promise or attach a catch, and keep the default throw rejection mode so unhandled rejections fail loudly in CI.

Frequently asked questions

What causes Node UnhandledPromiseRejection crashes the job in CI?
There are 2 common causes: a promise without await or catch and a missing await inside an async function. An async call is fired and forgotten; when it rejects there is no handler, so the default throw mode kills the process.
How do I fix Node UnhandledPromiseRejection crashes the job in CI?
There are 2 fixes depending on which cause you have: await and wrap in try/catch and add a global handler as a backstop. Work through them in order, since the first is the most common.
What does Node UnhandledPromiseRejection crashes the job in CI actually mean?
A node process prints UnhandledPromiseRejection (or an unhandled rejection stack) and exits non-zero.
How do I stop Node UnhandledPromiseRejection crashes the job in CI happening again?
Enable the no-floating-promises lint rule, await every promise or attach a catch, and keep the default throw rejection mode so unhandled rejections fail loudly in CI.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card