Node.js "--unhandled-rejections=strict" Changed CI Exit Behavior
Node’s --unhandled-rejections flag controls what happens when a promise rejects unhandled. A pipeline that set warn (or ran old Node) tolerated rejections; switching to strict/throw makes them fatal.
What this error means
After a Node upgrade or a changed NODE_OPTIONS, a job that previously only logged a warning now exits non-zero on the same unhandled rejection. Nothing in your code changed - only the rejection mode did.
# old behaviour (warn): job stayed green
(node:1) UnhandledPromiseRejectionWarning: TypeError: ...
# new behaviour (strict/throw): job fails
node:internal/process/promises:288 ... code: 'ERR_UNHANDLED_REJECTION'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.
# 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
The default mode changed across Node versions
Older Node warned on unhandled rejections; Node 15+ defaults to throw. A runner image bump to a newer Node flips the same code from warning to fatal.
NODE_OPTIONS sets a stricter mode
A pipeline that adds --unhandled-rejections=strict raises severity so even rejections inside the first tick of loading abort the process.
How to fix it
Fix the rejection rather than the mode
The correct response to a newly-fatal rejection is to handle it. Lowering the mode just hides a real bug.
- Find the unhandled rejection in the stack or the prior warning.
- Await it in
try/catchor attach.catch(). - Keep
throw/strictso future rejections stay visible.
Set the mode deliberately if you must
If you genuinely need to soften behaviour during a migration, set the mode explicitly so it is documented - but treat it as temporary.
# strict (recommended long-term)
node --unhandled-rejections=strict app.mjs
# or via env
NODE_OPTIONS=--unhandled-rejections=strictMake 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.
# pipefail is NOT set by default in every runner shell
- name: Build
shell: bash
run: |
set -euo pipefail
npm run build | tee build.logHow to prevent it
- Pin a Node version in CI so the default rejection mode does not change under you.
- Lint for floating promises so rejections never go unhandled in the first place.
- Keep
--unhandled-rejections=strictand treat any rejection as a bug.