Error Handling
Scope: exit codes, set -euo pipefail, and deliberate
failure. The difference between a script and a liability. All
examples were run with Bash 5.2.
Exit codes: the whole protocol
Every command exits with a number: 0 means success,
anything else means failure (1 is generic, others are
command-specific). $? holds the last one:
grep -q "x" file.txt
echo $? # 0 if found, 1 if not
if, while, &&, and || all test exit codes — that's the
entire branching mechanism. There are no exceptions, no error
objects. Just numbers.
set -euo pipefail: the three lines every script needs
#!/usr/bin/env bash
set -euo pipefail
Put this at the top of every script. What each flag does:
set -e— exit immediately if any command fails. Without it, Bash happily continues after errors, running later commands in a broken state.set -u— treat unset variables as errors. Catches typos ($nmae) and, critically, preventsrm -rf "$dir/"from becomingrm -rf /when$diris empty.set -o pipefail— a pipeline fails if any stage fails, not just the last. Without it,generate_data | uploadreports success even when generation failed.
The classic horror story these prevent:
dir="/tmp/work"
rm -rf "$dir/" # if dir were unset: rm -rf / — set -u stops this
Deliberate failure
[[ -f config.ini ]] || { echo "error: config.ini missing" >&2; exit 1; }
- Print errors to stderr (
>&2), not stdout — keeps error text out of pipelines and captured output. exit 1(or another non-zero code) signals failure to callers.{ ...; }groups commands; note the required spaces and final;.
For functions, return 1 instead of exit 1 (exit kills the
whole script).
Traps: cleanup on exit
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
trap ... EXIT runs the command when the script exits — for any
reason, including errors. It's how you clean up temp files
reliably. (Quote carefully: single quotes delay expansion until
the trap fires, which is usually what you want.)
set -e caveats (know these)
set -e is ignored in a few contexts — most notably the
condition of an if/while (that's fine, you're handling it)
and commands joined with &&/|| (also fine). The practical
rule: don't fight it, and never write set -e scripts that
depend on subtle behavior — keep error paths explicit with
|| exit 1.
Logging pattern
log() { echo "[$(date +%T)] $*"; }
err() { echo "[$(date +%T)] ERROR: $*" >&2; }
die() { err "$*"; exit 1; }
[[ -f "$1" ]] || die "input file missing: $1"
Small helpers, used everywhere. die is the standard name for
"print error and exit."
Check
- [ ] Every script you write starts with
set -euo pipefail - [ ] You can explain what each of the three flags prevents
- [ ] Errors go to stderr and failures
exitnon-zero
Next: Troubleshooting.