process.exitCode

Sets the status code the process will exit with once the event loop drains naturally.

Signature
process.exitCode
  • Added in Node.js v0.11.8

Return value

integer | string | null | undefinedThe exit status currently recorded for the process, or undefined if nothing has set one.

Errors

Assigning a value that is neither a number, an integer string, null nor undefined throws ERR_INVALID_ARG_TYPE.

Changelog

  • PHP v20.0.0Only accepts a code of type number, or of type string if it represents an integer. Previously invalid values were coerced silently.

process.exitCode is a writable property, not a function. Assigning to it records the status the process should report when it ends, without ending it. The event loop keeps running, timers fire, streams flush, and open handles are given the chance to close.

When to reach for it

Effectively always, in preference to process.exit(). A test runner sets it to 1 the moment a test fails and keeps going so it can print the full summary. A linter sets it after collecting every diagnostic. A batch job sets it in a catch block and still runs its cleanup.

What trips people up

  • Setting it does not stop execution. If the rest of your function continues and later succeeds, you must reset it to 0 yourself.
  • An explicit argument to process.exit(code) overrides whatever you assigned.
  • Since v20, process.exitCode = true or = 1.5 throws rather than being coerced.
  • An uncaught exception sets the status to 1, but an explicit assignment made beforehand still takes precedence over codes Node.js picks implicitly.

Exit codes are eight bits on POSIX: only 0–255 survive the trip to the shell.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Record a failing test in a custom runner while still printing the rest of the results.
  • Report a lint or type-check failure to CI after all diagnostics have been collected.
  • Fail a batch job from a catch block while the finally block still closes the database pool.

Frequently asked questions

Does setting process.exitCode stop the script?

No. It only records the status to use when the process eventually ends; the current function and the rest of the event loop keep running. Return from your function or throw if you also want execution to stop.

Should I use process.exitCode or process.exit()?

Use process.exitCode in almost every case, because it lets pending stdout writes, timers and cleanup handlers finish before the process ends. Reserve process.exit() for forcing a hung process to terminate.

What is the maximum value for process.exitCode?

On POSIX the shell only sees the low eight bits, so use 0 through 255; anything larger wraps around (256 becomes 0). Codes 1 through 125 are the safe range, as 126 and above are reserved by the shell for its own errors.

More examples

A test runner that reports every failure

The third test fails, but the loop still reports the summary line before the process ends. Had the catch block called process.exit(1), the run would have stopped at the first failure and the summary would never print. The shell still sees a non-zero status, so CI marks the step as failed.

Example · javascript
const tests = [
  ['adds', () => { if (1 + 1 !== 2) throw new Error('math is broken'); }],
  ['upper', () => { if ('a'.toUpperCase() !== 'A') throw new Error('bad case'); }],
  ['fails', () => { throw new Error('expected 3, got 4'); }],
];

let failed = 0;

for (const [name, fn] of tests) {
  try {
    fn();
    console.log(`  ok   ${name}`);
  } catch (err) {
    failed++;
    console.log(`  FAIL ${name}: ${err.message}`);
  }
}

console.log(`\n${tests.length - failed} passed, ${failed} failed`);
process.exitCode = failed > 0 ? 1 : 0;

// Output:
//   ok   adds
//   ok   upper
//   FAIL fails: expected 3, got 4
//
// 2 passed, 1 failed
//
// $ echo $?  ->  1

Failing while cleanup still runs

Setting exitCode inside catch lets the finally block complete its await before the process ends, so the lock file is always removed. process.exit(1) in the same place would kill the process mid-await and leave the lock behind, blocking the next run.

Example · javascript
import fs from 'node:fs/promises';

async function main() {
  const lock = 'import.lock';
  await fs.writeFile(lock, String(process.pid));

  try {
    await importRecords();
    console.log('import complete');
  } catch (err) {
    console.error('import failed:', err.message);
    process.exitCode = 1;      // status recorded...
  } finally {
    await fs.rm(lock);          // ...and this still runs
    console.log('lock released');
  }
}

async function importRecords() {
  throw new Error('connection reset');
}

await main();

// Output:
// import failed: connection reset
// lock released
//
// $ echo $?  ->  1

What the shell sees in each case

An explicit argument to exit() always wins; a bare exit() falls back to whatever exitCode holds. The final block shows the v20 tightening: booleans and floats used to be coerced into a status, and now raise ERR_INVALID_ARG_TYPE instead, which surfaces bugs that previously exited 1 by accident.

Example · javascript
// $ node -e 'process.exitCode = 9'; echo $?
// 9

// $ node -e 'process.exit(42)'; echo $?
// 42

// $ node -e 'process.exitCode = 9; process.exit(42)'; echo $?
// 42

// $ node -e 'process.exitCode = 9; process.exit()'; echo $?
// 9

// Since v20, invalid values throw rather than being coerced:
try {
  process.exitCode = true;
} catch (err) {
  console.log(err.code); // ERR_INVALID_ARG_TYPE
}

Discussion

  • Be the first to comment on this lesson.