Exit Codes
Use $?, set -e/u/pipefail, trap ERR and EXIT to write robust scripts that fail loudly and clean up after themselves.
- Read and act on exit codes with $?
- Enable strict mode with set -euo pipefail
- Register cleanup logic with trap EXIT
- Catch unexpected errors with trap ERR
- Explain why pipefail matters for pipeline reliability
A command that fails silently is worse than a command that fails loudly. The default Bash behaviour is to keep running even when a command exits with an error — which means a script can reach the end having done half its job and destroyed something along the way. Exit codes and signal traps are the mechanism Bash provides to turn silent failures into explicit errors.
Exit codes: $?
Every command exits with a numeric code. 0 means success; any non-zero value means failure. The variable $? holds the exit code of the most recently executed command:
ls /etc/hosts
echo "Exit code: $?" # 0 — file exists
ls /nonexistent
echo "Exit code: $?" # 2 (or similar) — file not foundConditional logic is built on exit codes — if, &&, and || all operate on them:
if grep -q "ERROR" app.log; then
echo "Errors found"
fi
mkdir -p /tmp/work && echo "Directory ready"
cp config.json /tmp/work || { echo "Copy failed" >&2; exit 1; }set -e: exit on error
set -e (or set -o errexit) causes the script to exit immediately when any command returns a non-zero exit code:
#!/usr/bin/env bash
set -e
cd /nonexistent # script exits here with an error
echo "This never runs"Without set -e, the script would continue past the failed cd, potentially running subsequent commands in the wrong directory. This is a common source of subtle bugs in deployment and backup scripts.
set -e has edge cases. It does not trigger inside if conditions, while/until conditions, or commands that are negated with !. It also does not catch failures in the middle of a pipeline — that requires set -o pipefail (below). Know these limits and test your error paths.
set -u: fail on unset variables
set -u (or set -o nounset) causes the script to exit when you expand an unset variable:
#!/usr/bin/env bash
set -u
echo "User is: $UNDEFINED_VAR" # exits: UNDEFINED_VAR: unbound variableThis catches typos in variable names immediately, rather than letting them silently expand to empty strings and cause confusing downstream failures.
Use a default value when a variable is legitimately optional:
output_dir=${OUTPUT_DIR:-/tmp/output}set -o pipefail
By default, a pipeline's exit code is the exit code of the last command in the pipeline. This means earlier failures are silently swallowed:
# Without pipefail, this exits 0 because sort succeeds:
cat /nonexistent | sort
echo "Exit: $?" # 0 — the failure is invisibleset -o pipefail changes this: the pipeline exits with the code of the rightmost failing command:
set -o pipefail
cat /nonexistent | sort
echo "Exit: $?" # 1 (or 2) — cat's failure is surfacedIn practice, use all three together at the top of every production script:
#!/usr/bin/env bash
set -euo pipefailtrap EXIT: guaranteed cleanup
trap registers a command (or function) to run when the script exits — whether normally, on error, or when interrupted. Use it to clean up temporary files:
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
# Work in tmpdir — cleanup runs no matter how the script ends
cp config.json "$tmpdir/"
process_files "$tmpdir"Without trap EXIT, a script killed by Ctrl-C or an error would leave the temp directory behind.
trap ERR: log unexpected failures
trap ERR fires when any command fails (subject to the same conditions as set -e). Use it to log context before the script exits:
#!/usr/bin/env bash
set -euo pipefail
on_error() {
echo "[ERROR] Script failed at line $LINENO" >&2
}
trap on_error ERR
# ... rest of script$LINENO expands to the current line number, making it easy to locate the failure.
Combine trap ERR and trap EXIT. ERR fires first (logs the error), then EXIT fires (cleans up). Multiple traps can be stacked on the same signal by building up the trap command, or by calling a function from the trap that handles multiple concerns.
Check your understanding
- 1.Without set -o pipefail, what is the exit code of: cat /missing_file | sort
- 2.You want a temp directory to be deleted whenever your script exits, including on Ctrl-C. Which trap signal should you use?
- 3.With set -u, the expansion ${VAR:-default} causes the script to exit if VAR is unset.
Do it yourself
#!/usr/bin/env bash
set -euo pipefail
tmpdir=$(mktemp -d)
trap 'echo "Cleaning up $tmpdir"; rm -rf "$tmpdir"' EXIT
echo "Working in $tmpdir"
echo "hello" > "$tmpdir/test.txt"
cat "$tmpdir/test.txt"
# Uncomment to see exit-on-error in action:
# cat /nonexistentSave this as test-strict.sh, make it executable (chmod +x test-strict.sh), and run it. Then uncomment the failing line and run again to see the error exit.
Where to go next
You can now write scripts that fail loudly and clean up reliably. The Scripting patterns lab is next — apply everything from conditionals through exit codes in hands-on quiz challenges, then move on to Text processing to learn grep, sed, and awk.