---
title: 'Exit Status'
source: 'https://academia.sh/en/courses/shell-programming/exit-status'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:02+00:00'
license: 'CC BY-SA 4.0'
---

# Exit Status

The success/failure convention, reserved code ranges, conditional chaining operators, and why these operators are not confused with conditional branching.

In the previous lessons, commands' results always came out as expected. In
reality, `grep` may find no match, `cut` may fail to open the file, `sort`
may not find enough temporary space. The shell's way of learning about these
situations is a single number.

Every process leaves an integer with the kernel when it terminates. The shell
takes this number as the **exit status** and stores it in the special
variable `$?`. Shell programming's control flow — conditions, loop
conditions, error handling — is built entirely on this number.

## Why Zero Is Success

The convention is this: **0 is success, every nonzero value is failure.**

In the Programming Fundamentals course, the value `true` was used for
logical correctness; the shell's convention looks like the reverse of this.
The reason is simple: a job succeeding has exactly one meaning, while its
failing has many causes. The 255 nonzero values can be used to distinguish
different kinds of failure; a single value suffices for success.

`grep` is a clear example of this distinction, and it separates three codes
from each other:

```sh
grep -q '404' access.log;      echo "found      -> $?"
grep -q '999' access.log;      echo "not found  -> $?"
grep -q '404' missing-file.log 2>/dev/null; echo "file missing -> $?"
```

```
found      -> 0
not found  -> 1
file missing -> 2
```

All three differ from each other, and this difference does real work. "No
match" is an expected result; "file missing" is a situation where the script
should stop. A script that looks only at "zero or not" confuses the two.

The same rigor is expected in your own scripts: give different codes to
different error classes, do not use code `1` for everything.

## Commands That Embody the Convention

The `true` and `false` builtins do no work at all; they only return a code.

```sh
true;  echo "true  -> $?"
false; echo "false -> $?"
```

```
true  -> 0
false -> 1
```

The `:` command is equivalent to `true`, and it was used in the previous
lesson to empty a file.

The `test` command, and its bracket notation `[ ... ]`, tests a condition and
returns the result as a code. This command has no output at all; all the
information is in the code.

```sh
[ -f access.log ]; echo "file exists  -> $?"
[ -f missing.log ];    echo "file missing -> $?"
```

```
file exists  -> 0
file missing -> 1
```

`[` is not a syntax element but an executable command name; this is why the
spaces around it are mandatory, and the closing `]` is its last argument.
The detail of tests will be covered in the Conditions and Tests lesson.

## The Code's Numeric Limits

An exit status fits into a byte, in the range 0–255. Values outside this
range are reduced modulo 256:

```sh
(exit 300); echo "300 -> $?"
(exit -1);  echo "-1  -> $?"
```

```
300 -> 44
-1  -> 255
```

$300 \bmod 256 = 44$ and $-1 \bmod 256 = 255$. Returning a code greater than
255 in your script has no point; the number silently turns into something
else.

## Reserved Codes

The shell reserves some codes for its own use. Giving these codes to your own
error classes leads your script to mislead its caller.

```sh
nosuchcommand 2>/dev/null; echo "command not found -> $?"
./unrunnable.txt   2>/dev/null; echo "not executable    -> $?"
```

```
command not found -> 127
not executable     -> 126
```

`127` reports that the command was not found in the search path; `126` that
it was found but could not be run (no execute permission, or it is a
directory). If you get 127 when calling a script, the problem is not inside
the script but in its name or the search path.

For processes that terminate by a signal, the shell produces a code in the
form **128 + signal number**:

```sh
bash -c 'trap - INT; kill -INT $$'; echo "INT  -> $?"
bash -c 'kill -TERM $$';            echo "TERM -> $?"
```

```
INT  -> 130
TERM -> 143
```

`SIGINT` is number 2 ($128 + 2 = 130$), `SIGTERM` is 15 ($128 + 15 = 143$).
This is why a script interrupted from the keyboard returns 130. Signals and
catching them are covered in detail in the Writing Robust Scripts topic.

The usable range in practice is 1–125. If the `exit` command is called with
no argument, it returns the code of the last command that ran in the script.

## Conditional Chaining

The shell defines two chaining operators that depend on the exit status.

The `&&` operator runs the right-hand command if the left-hand one
**returns 0**. The `||` operator runs the right-hand command if the
left-hand one returns **nonzero**. In both cases, if the right-hand command
does not run, the chain's code is the left-hand one's code.

```sh
grep -q '404' access.log && echo "log has 404"
grep -q '999' access.log || echo "999 does not appear"
```

```
log has 404
999 does not appear
```

These operators perform **short-circuit evaluation** — the same behavior
defined for logical operators in the Programming Fundamentals course. The
difference is that what is being evaluated here is not an expression but a
process being run: short-circuiting does not just save time, it ensures a
job is never done at all.

The safe in-place transformation pattern from the previous lesson was written
with this operator:

```sh
sort small.txt > small.new && mv small.new small.txt
```

If `sort` fails, `mv` never runs, and the original file stays in place.
Tying a destructive operation to a success condition is the plainest form of
error handling in the shell.

## Chaining Is Not Conditional Branching

`&&` and `||` have **the same precedence** and associate **left to right**.
These two rules give rise to a common misunderstanding: the notation
`condition && body || fallback` is thought to be equivalent to `if`–`else`.
It is not.

```sh
fail() { echo "body ran"; return 1; }

true && fail || echo "FALLBACK RAN"
```

```
body ran
FALLBACK RAN
```

The condition was true, the body ran — but because the body reported
failure, the fallback ran too. `if`–`else` does not behave this way:

```sh
if true; then fail; else echo "FALLBACK"; fi
```

```
body ran
```

Rule: **an `&&` … `||` chain substitutes for `if`–`else` only when the body
can never fail.** Because this guarantee can rarely be given, writing `if`
for a two-branch choice is the correct default. `&&` and `||` are suited to
single-branch guards — "continue if it succeeded," "warn and exit if it
failed."

Left-to-right associativity also shows itself in mixed chains:

```sh
false || echo "first" && echo "second"
```

```
first
second
```

`false || echo "first"` is evaluated first; because `echo` succeeds, the
chain's code is 0, and `&&` runs the right-hand side. Use grouping instead
of hard-to-read chains: the code of a command list inside `{ ...; }` is the
code of the list's last command.

## Negation

The `!` prefix reverses a command's code: it turns 0 into nonzero, and
nonzero into 0.

```sh
! grep -q '999' access.log; echo "negation -> $?"
```

```
negation -> 0
```

This is the direct way to express the condition "if 999 does not appear in
the log." Negation has a second function too: suppressing expected failures
under strict mode (`set -e`). This use is covered in the Error Handling
lesson.

## Wrapping Up the Topic

The four concepts established in this topic form the vocabulary for the rest
of the course. The pipeline that extracts the five most requested paths from
the log uses this entire vocabulary:

```sh
[ -r access.log ] || { echo 'log unreadable' >&2; exit 2; }
cut -d' ' -f7 access.log | sort | uniq -c | sort -rn | head -5
```

The first line contains an exit status test, a chaining operator, a
grouping, and a redirection to standard error. The second line is a
four-command pipeline. Together, the two form the core of the `report.sh`
script to be written in the next topic.

## Summary

- Every process leaves an exit status in the range 0–255 when it
  terminates; 0 is success, every nonzero value is failure. The code is
  read with `$?`.
- Different kinds of failure are distinguished by different codes; `grep`
  returns 1 for no match, 2 for a file error.
- 126, 127, and 128 + signal number are reserved by the shell; the range
  1–125 is used for your own codes.
- `&&` and `||` perform short-circuit evaluation; they have the same
  precedence and associate left to right.
- The notation `condition && body || fallback` is not equivalent to
  `if`–`else` when the body can fail.

## Next Step

Everything in this topic was done with single-line commands. When the same
pipeline needs to run every day, gain options, and be handed off to someone
else, a single line is not enough. The next topic builds the script file: it
opens by covering what the interpreter declaration does, how a script
becomes executable, and which shell is used when the system runs a file.
