Basics

What is an exit code — and why CI can be green while tests fail

Illustration: a courier walks out of a door holding a small number plate while a strict inspector waits outside

Here's a story that happens to everyone. You add test-log saving to CI: npm test | tee test.log. Handy. A week later you find out three tests have been failing for days, and CI stayed green the whole time.

The tests honestly failed. The log honestly recorded the errors. What broke was something you never saw — the exit code. Let's see what it is and how it got lost.

An exit code is one number at the end

Every program, when it finishes, reports one number from 0 to 255 to the system. That's the exit code.

The rule is simple:

  • 0 — everything went fine.
  • Any other number — something went wrong.

Think of a courier who comes back not with a long report but with a single plate. Zero means "delivered". Non-zero means "problem". The program writes the error text separately, to its output. The plate is what other programs read.

To see the code of the last command in the terminal:

npm test
echo $?        # 0 — tests passed, 1 — they failed

In PowerShell on Windows, use $LASTEXITCODE instead of $?.

Who looks at it

In the terminal you usually read text. Machines only look at the number:

  • && in commands. npm test && npm run deploy ships the site only if the tests returned 0.
  • CI/CD. A pipeline step is green on zero and red on any other code. CI doesn't care about the word "FAILED" in the log.
  • Docker and servers. A container gets restarted or marked as crashed based on its exit code.
  • AI agents. When an agent runs a command, it checks the code. Zero — move on; non-zero — read the error and fix it.

The code is visible in CI logs too. In GitHub Actions a failed step ends with the line Process completed with exit code 1. If the text shows red tests but that line is missing and the step is green, the code got lost along the way. Look for it in the step's script itself.

Why the code got lost in the pipe

Back to the start. The | sign is a pipe: the first command's output feeds the second one. But the whole pipe has one exit code — the last command's.

npm test | tee test.log
echo $?        # 0 — that's tee's code, not the tests'

tee wrote the file successfully and returned 0. The tests' code (1) was simply thrown away. CI saw zero and was happy.

The fix is one line at the top of the script:

set -o pipefail
npm test | tee test.log
echo $?        # 1 — now the pipe tells the truth

With pipefail, the pipe returns an error if any part of it failed. Add it to every bash script that has a |.

Two more patterns do the same damage — and AI writes both often:

  • || true at the end of a command. It means "even if it failed, count it as success". Sometimes useful, but on a test step it switches off the whole check.
  • A try/catch that only prints the error. The script caught the exception, printed "Error!" and calmly exited with zero. To CI, it succeeded.

What the common codes mean

The numbers are a convention, but a few show up all the time:

  • 1 — a general error. That's how Python or Node exit when an exception goes uncaught.
  • 127 — command not found. A typo, or the program isn't installed.
  • 126 — the file exists but can't be run: no execute permission.
  • 130 — you pressed Ctrl+C. It's computed as 128 + the signal number (2).
  • 137 — the process was force-killed: 128 + 9. In Docker it almost always means one thing — the container ran out of memory.

See exited with code 137 in your logs? Don't hunt for a bug in your code. Hunt for whatever ate the memory: check docker stats or raise the container's memory limit.

How do I set my own exit code?

In Python — sys.exit(2), in Node — process.exitCode = 2. Handy when your script checks something for CI: found a problem — return non-zero and the pipeline stops.

Why do tests fail in CI but pass locally?

That's a different story — about the environment, not codes. The usual causes are in why tests pass locally but fail in CI.

Are the exit code and the error text the same thing?

No. The program writes text to a separate error stream — stderr. More on that in what are stdout and stderr. The exit code is just a number at the very end.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →