# What are exit codes?

An exit code is the number a program returns when it finishes, where 0 usually means success and any other value signals an error that scripts can check.

Last updated September 29, 2026, 8 min read

## Learning objectives

After reading this article you will be able to:

-   Define exit codes and exit status
-   Explain common exit code conventions
-   Identify how tests, CI, and agents rely on exit codes

## Related content

-   [How to test a command-line application](https://specstory.com/learning/cli-and-web/testing-command-line-applications)
-   [What is the difference between stdout and stderr?](https://specstory.com/learning/cli-and-web/stdout-vs-stderr)
-   [What is a headless browser?](https://specstory.com/learning/cli-and-web/headless-browser)

## What are exit codes?

An exit code is a number that a finished program reports to the process that started it, usually 0 for success. It is also called the exit status or return code. Any other value usually means a failure. Shell scripts and build systems read that number and branch on it.

The [Bash reference manual](https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html) states that exit statuses fall between 0 and 255, and that the shell gives a few values above 125 a special meaning. A shell does not print the code unless someone asks for it, e.g. with `echo $?`. On Windows, the code is a 32-bit number, which `cmd` shows as `%ERRORLEVEL%` and PowerShell as `$LASTEXITCODE`.

A command that fails but exits 0 lets the next step run on bad data. A test of a [command-line application](https://specstory.com/learning/cli-and-web/testing-command-line-applications) usually checks the exit code for that reason.

## How do exit codes work?

On Linux and macOS, an exit code reaches the parent process in four steps:

1.  The program ends by calling `exit` with a status, e.g. `exit(1)` in C. Returning a value from `main` has the same effect.
2.  The kernel holds the status until the parent collects it, and the parent's wait call gets only the low 8 bits.
3.  The parent process, usually a shell, waits for the program and reads the status.
4.  The shell stores the code in the special parameter `$?`, and conditional commands branch on it, e.g. `if`.

If a signal ends the program instead, the status records the signal number, and Bash reports 128 plus that number.

Diagram: How an exit code reaches the parent shell

A normal exit and a fatal signal both end as one number in the parent shell. A code above 128 usually points to a signal.

The [exit(3) manual page](https://man7.org/linux/man-pages/man3/exit.3.html) states that the parent receives only the lowest byte of the status. A program that calls `exit(256)` therefore reports 0, and one that calls `exit(-1)` reports 255.

Continuous integration (CI) systems branch on the exit code of each step in a [CI pipeline](https://specstory.com/learning/ci-cd/ci-cd). [GitHub Actions](https://specstory.com/learning/ci-cd/github-actions-checks) fails a step whose command exits nonzero and, by default, skips the steps after it. A step that exits with 1 logs `Error: Process completed with exit code 1.`

## What is an example of exit codes in a script?

Here is an illustrative example. Acme Co. sells furniture online. A developer at Acme writes a nightly script that exports the day's orders with Acme's command-line tool and uploads the file only if the export exits 0:

```bash
#!/usr/bin/env bash
if ! acme export --format csv > orders.csv; then
  echo "export failed, upload skipped" >&2
  exit 1
fi
acme upload orders.csv
```

One night the order database is down. The script runs in five steps:

1.  The shell creates an empty `orders.csv` for the redirect, then starts `acme export`.
2.  The export cannot reach `db.example.com`. It writes an error to [stderr](https://specstory.com/learning/cli-and-web/stdout-vs-stderr) and exits with 1.
3.  The `!` turns the export's status of 1 into 0, so the `then` branch runs.
4.  The script writes `export failed, upload skipped` to stderr and exits with 1, so `acme upload` never runs.
5.  The scheduler receives exit code 1 and marks the nightly job as failed.

```text
$ ./nightly.sh
acme: cannot connect to db.example.com
export failed, upload skipped
$ echo $?
1
```

Without the `if` check, the script would upload the empty file and exit with the upload's status of 0, so the job would pass. A script that starts with `set -euo pipefail` stops at the first failing command, but `set -e` ignores failures in `if` conditions and in commands joined by `&&` or `||`. This example is simplified. A real script would also check that `orders.csv` is not empty and use a separate exit code for each kind of failure.

## What do common exit codes mean?

These values appear most often in shells and CI logs:

| Code | Usual meaning | Set by |
| --- | --- | --- |
| 0 | Success | The program |
| 1 | General failure | The program |
| 2 | Incorrect usage, e.g. an unknown option | Bash builtins and many tools |
| 126 | Command found but not executable | The shell |
| 127 | Command not found | The shell |
| 130 | Ended by Ctrl+C, which sends SIGINT (signal 2) | The shell, as 128 plus 2 |
| 137 | Ended by SIGKILL (signal 9) | The shell, as 128 plus 9 |
| 255 | A status out of range, e.g. `exit -1` | The program, cut to its low 8 bits |

Python and Node.js return 1 when an error goes uncaught. The Portable Operating System Interface (POSIX) standard fixes 126 and 127 for every shell that conforms to it.

POSIX requires only a value above 128 when a signal ends a command. The Linux kernel sends SIGKILL when it ends a process to free memory, so 137 in a [container](https://specstory.com/learning/environments/containers-vs-gvisor-vs-microvms) log often means the process ran out of memory. Code 139 is 128 plus 11, the SIGSEGV signal of a segmentation fault. Code 141 is 128 plus 13, the [SIGPIPE](https://specstory.com/learning/cli-and-web/broken-pipe) signal, which ends a program that writes to a pipe whose reader has closed. Code 143 is 128 plus 15, the SIGTERM signal that `kill` sends by default.

Each program documents any other codes it returns:

-   **grep.** GNU grep exits 0 when it selects a line, 1 when it selects none, and 2 when an error occurs.
-   **pytest.** pytest exits 1 when some tests fail and 5 when it collects no tests.
-   **curl.** curl exits 22 when its `--fail` option is set and the server returns an HTTP status of 400 or above.

## What changes when a coding agent writes the code?

A [coding agent](https://specstory.com/learning/ai-coding/coding-agent) runs commands through a shell tool and gets back their output and exit code. Its next step after a build or a test run depends on that code. An agent can shorten long output with a pipe, e.g. `pytest | tail -n 1`. Bash then reports the exit code of `tail`, which is 0, even when a test fails:

```text
$ pytest | tail -n 1
========================= 1 failed, 41 passed in 0.84s =========================
$ echo $?
0
```

A script or CI step that reads only the exit code then records a pass, although the summary line reports a failure. An agent's report that relies on that code alone can say "all tests pass," which is a [false completion claim](https://specstory.com/learning/verification/coding-agent-done-claims). Code that an agent writes can hide failures the same way, e.g. an error handler that prints a message and lets the program exit 0.

A team can run agent commands with `set -o pipefail`, which makes a pipeline return a nonzero code when any command in it fails. A team can also add a test that expects a nonzero code from each failure path. A failure report that keeps the exact command and its exit code gives a person [reproduction steps](https://specstory.com/learning/debugging/reproduction-steps) to rerun.

## What does exit code 0 not prove?

Exit code 0 shows only that a program reported success. A nonzero code works as an implicit [test oracle](https://specstory.com/learning/test-quality/test-oracle), a failure signal that needs no expected value. Exit code 0 misses anything the program does not report, in three ways:

-   **It does not check the output.** A program can exit 0 after it writes a wrong total or an empty file. A [smoke test](https://specstory.com/learning/testing/smoke-testing) that checks only that `acme --version` exits 0 shows that the program starts, not that its exports are right.
-   **It can come from the wrong command.** In GitHub Actions on Linux, a `run` step with no `shell` setting uses `bash -e` without `pipefail`. A piped test command then reports the last command's code. Setting `shell: bash` adds `pipefail`.
-   **It can hide failed or skipped work.** Adding `|| true` after a command makes the line return 0 even when the command fails. A test run that skips its tests can exit 0 as well. Both produce a [false pass](https://specstory.com/learning/verification/false-pass).

## How is an exit code different from an HTTP status code?

An exit code reports how a whole process ended, once, to the process that started it. An HTTP [status code](https://specstory.com/learning/glossary#status-code) reports the result of one request, from a web server to a client, while the server keeps running. HTTP status codes have standard meanings grouped by their first digit, e.g. 404 for not found. For exit codes, standards fix only 0 and a few shell values, e.g. 127, and each program defines the rest.

Success is also a different number, 0 for a process and usually 200 for HTTP.

## FAQs

### Are there standard exit status codes in Linux?

Standard exit status codes in Linux cover only a few values. Zero means success, and shells use 126 for a command that is not executable and 127 for one that is not found. Bash reports 128 plus the signal number after a fatal signal.

### What does "returned exit code 0" mean?

The message "returned exit code 0" means that the program finished and reported success to the tool that started it, e.g. a code editor. Exit code 0 does not show that the program's output was correct.

### How do you check an exit status in an if statement?

An if statement in Bash checks an exit status when the command itself is the condition. The then branch runs when the command exits with 0, and the else branch runs for any other code. An exclamation mark before the command negates the status.

### What does "Process completed with exit code 22" mean?

The message "Process completed with exit code 22" is the error GitHub Actions logs when a step's command exits with 22. That code often comes from curl with its fail option, which exits with 22 when the server returns an HTTP status of 400 or above.

---

Source: [What do CLI exit codes mean? | Exit code 0 and 1 | SpecStory](https://specstory.com/learning/cli-and-web/exit-codes)
