Where to start First steps track · 15 stages
Linux Labs

Exit codes: $?, && and ||

10 min Ubuntu 24.04 · bash 5.2

An exit code is a number from 0 to 255 that a command reports: 0 means success, anything else a failure or a "no".

Cheat sheet

8 commands Printable version
  • ls /nope; echo $? 2 — code of the last command
  • grep -q root /etc/passwd && echo found second runs after code 0
  • mkdir /srv/app || echo "failed" >&2 second runs after non-zero
  • if grep -q '^backup:' /etc/passwd; then echo found; fi
  • rc=$? save the code before it's lost
  • exit 1 leave a script with code 1
  • false | true; echo "${PIPESTATUS[@]}" 1 0 — code of every stage
  • set -o pipefail a failed stage fails it all

Overview

When a command finishes, it hands the shell an exit code — an integer from 0 to 255. Zero means success; any other number means a failure or the answer "no". The code is not printed: it sits in the $? variable until the next command runs.

text
student@ubuntu:~$ ls /nope
ls: cannot access '/nope': No such file or directory
student@ubuntu:~$ echo $?
2

The error text is for people and goes to standard error. Programs read only the code: if, &&, ||, while, systemd, cron and monitoring.

Code Meaning
0 success
1 general failure; for grep "no match", for diff "files differ"
2 usage error: ls found no such path, grep could not open a file
124 timeout stopped the command
126 the file exists but cannot run: no x permission
127 command not found
128+N killed by signal N: 130 is Ctrl+C, 137 is kill -9, 143 is kill

A code of 128+N means a signal number N ended the process.

Small codes like 1 and 2 mean different things for each program; the EXIT STATUS section of its man page lists them. For curl, 6 is "could not resolve the host", 7 is "could not connect", 28 is "timed out", and 22 is an HTTP response of 400 or above when -f is given.

&& and ||

a && b runs b only if a returned 0. a || b runs b only if a returned non-zero. The whole line returns the code of the last command that ran. A ! before a command swaps success and failure: ! true gives 1.

A script's code

A script exits with the code of the last command it ran unless it hit exit N first. A bare exit passes on the previous command's code. Codes wrap modulo 256: exit 256 gives 0, exit 300 gives 44.

A pipeline's code

A pipeline returns the code of its last stage, so false | true returns 0. The codes of all stages are in the PIPESTATUS array. After set -o pipefail a pipeline returns the code of the rightmost stage that failed, and 0 only if every stage succeeded.

Recipes

6

Test a command in if

bash
if grep -q '^backup:' /etc/passwd; then
  echo "user backup exists"
fi

if reads the command's code itself; $? is not needed. The form cmd; if [ $? -ne 0 ] works until someone puts an echo between the two.

Save the code before it is overwritten

bash
curl -s -o /dev/null --max-time 5 http://127.0.0.1:8080/
rc=$?
echo "curl returned $rc"

Every command overwrites $? — echo, [ ], an assignment with a substitution. If you need the code twice, store it in a variable right away.

Exit with an error message

bash
die() { echo "$*" >&2; exit 1; }
[ -r "$conf" ] || die "no file $conf"
cd /srv/app || die "no directory /srv/app"

The message goes to standard error (>&2) and the code is non-zero, so both a person and the calling program understand the script.

curl's code and the HTTP response

curl's code answers whether an exchange with the server took place. A 503 response is an exchange too, so the code is 0. The -f option turns responses of 400 and above into a failure (code 22), while 204 and 302 still count as success. This is how a test server answered:

Server response curl -s curl -sf curl -s -w '%{http_code}'
200 0 0 200
204, 302 0 0 204, 302
404, 503 0 22 404, 503
port closed 7 7 000

When you need the exact response code, print it with -w '%{http_code}' and compare it in a test. With -o and without -s, curl draws a progress meter on standard error.

Your own codes for different failures

bash
[ $# -eq 1 ] || { echo "Usage: check.sh URL" >&2; exit 2; }
curl -sf -o /dev/null "$1" || exit 1

The usual convention: 1 means the check failed, 2 means the script was called wrongly. Stay below 126: 126, 127 and 128+N already carry a meaning in the shell.

The code of every pipeline stage

text
student@ubuntu:~$ false | true; echo "rc=$? ps=${PIPESTATUS[*]}"
rc=0 ps=1 0
student@ubuntu:~$ set -o pipefail; false | true; echo "rc=$?"
rc=1

Troubleshooting

7

The script prints OK while the service is down

The check reads the code of the wrong command:

text
student@ubuntu:~$ curl -s http://127.0.0.1:1/; echo OK; echo "rc=$?"
OK
rc=0

echo OK ran after curl and put its own zero into $?. Test the command itself: if curl -sf …; then echo OK; fi.

A line in the middle failed, yet the script returned 0

A script returns the code of its last command. This is the journal of a service (journalctl -u clean.service -o cat) whose script failed on its second line while the last command (find) succeeded:

text
/usr/local/bin/clean.sh: line 2: report-tool: command not found
clean.service: Deactivated successfully.

Add || exit 1 after the commands that matter, or set -e at the top of the script (see bash-scripting).

The script did its job but returned 1

The same cause from the other side. A last line like [ "$DEBUG" = yes ] && echo "done" returned 1 because DEBUG was empty, and the script returned 1 with it, even though the work was done. A service that runs such a script ends up failed. Finish the script with an explicit exit 0 or rewrite the line with if.

Code 127

command not found: the command is not in PATH. A script that works by hand but fails from cron or a timer usually hits their shorter PATH (see the timer environment). Use the full path to the program.

Code 126

The file was found but cannot run: bash: ./check.sh: Permission denied. The x permission is missing (chmod +x check.sh) or the filesystem is mounted noexec.

Code 130, 137 or 143

A signal stopped the command: 130 is Ctrl+C, 143 is kill or systemctl stop, 137 is kill -9, timeout -s KILL or running out of memory. Look for the sender in the journal: sudo journalctl -k | grep -i oom.

grep stops a script that uses set -e

grep returns 1 when nothing matches. set -e and pipefail treat that as a failure and the script exits. If "no match" is a normal outcome, write grep … || true or test it in an if.

Danger zone

2

a && b || c instead of if

c also runs when b fails:

text
student@ubuntu:~$ true && false || echo "c ran"
c ran

Where it must be one or the other (delete or report), write if a; then b; else c; fi.

An error count in the exit code

exit $errors with 256 errors returns 0 — success. The code is not a counter: exit with 1 on any error and print the count.

Terms

1
exit status
A command result: zero normally means success; a nonzero value needs interpretation. Also appears in: pgrep, ping, whoami

See also

  • bash-scripting — set -euo pipefail, conditions and loops in scripts.
  • test-conditions — [ ] and [[ ]] tests, which also answer with a code.
  • redirection — standard error, >&2 and pipelines.
  • curl — requests for checking services.
  • kill — the signals behind code 128+N.
  • systemctl — a service's code in systemctl status.
  • shellcheck — catches $? after an extra command and cd without || exit.
Loading…