Exit Codes & Error Handling
Every command that finishes hands back a number saying whether it succeeded — zero for success, anything else for failure — and a handful of shell options turn that convention into real error handling.
$? — the last command's exit code
$ terminal
grep "ERROR" access.log echo "Exit code: $?"
Terminal output
2026-09-08 ERROR disk full Exit code: 0
0 always means success. Any non-zero value (1 through 255) means some kind of failure — grep specifically returns 1 when it finds no matching lines at all, which is why checking $? after a command is a normal way to branch on whether it worked.
Checking a command's success directly
bash check.sh
#!/bin/bash if cp source.txt backup.txt; then echo "Backup succeeded" else echo "Backup failed" fi
Terminal output
Backup succeeded
An if can test a command directly, not just a [[ ]] expression — it treats exit code 0 as "true" and anything else as "false."
set -e, set -u, and pipefail
By default, Bash keeps going even after a command fails partway through a script. These options change that:
bash safe-script.sh
#!/bin/bash set -euo pipefail echo "Starting..." cp missing-source.txt destination.txt echo "This line never runs"
Terminal output
Starting... cp: cannot stat 'missing-source.txt': No such file or directory
set -e— stop the whole script immediately if any command fails.set -u— treat using an undefined variable as an error, instead of silently substituting an empty string.set -o pipefail— make a pipeline's exit code reflect its last failing command, instead of always just the exit code of the final command in the pipe.
What set -e doesn't catch: a failing command inside an
if/while condition, or on the left side of &&/||, does not trigger set -e — those contexts are specifically exempted, since the whole point of testing a command's success is to handle failure yourself. It also doesn't catch a failing command inside a pipeline unless pipefail is also set. Treat set -euo pipefail as a strong safety net, not a guarantee that every failure will be caught — explicit checks on commands you really care about are still worth writing.