Topic 6.4
Error Handling & Debugging Scripts
In one line
set -e, set -u, and checking exit codes are what separate a script that silently plows ahead after something breaks from one that fails loudly and immediately — and bash -x is how you watch a broken script's every step.
Key ideas
- 01
By DEFAULT, bash keeps executing a script even after a command fails — a script with five sequential steps where step 2 fails will still attempt steps 3, 4, and 5 as if nothing went wrong, which is very often NOT what you actually want, and a genuinely common source of scripts that fail in confusing, hard-to-trace ways.
- 02
set -e(often placed right after the shebang) makes the ENTIRE script exit immediately the moment any command fails (returns a non-zero exit code) — a single line that converts a script from 'silently keep going after failure' to 'stop immediately and clearly at the first sign of trouble,' which is almost always the safer default for any script beyond a few trivial lines. - 03
set -umakes the script exit immediately if it tries to use a variable that was NEVER SET — catching typos in variable names ($NAMinstead of$NAME) immediately, rather than silently treating the mistyped variable as an empty string and continuing with wrong, unexpected behavior further down the script. - 04
set -euo pipefail(a genuinely standard, widely-recommended combination) adds one more piece: withoutpipefail, a pipeline's exit code is only the LAST command's — meaningcat missing-file.txt | grep foocould report success (grep's exit code) even thoughcatitself clearly failed.pipefailmakes the whole pipeline fail if ANY command within it fails, not just the last one. - 05
Explicitly checking
$?(a command's exit code, from Phase 3) after anything genuinely critical, even withset -ealready active, is worth doing for commands where you want a SPECIFIC, informative error message rather than the script just stopping with a generic failure —command || { echo "specific error message"; exit 1; }is a common, readable pattern for this. - 06
bash -x script.sh(or addingset -xinside the script itself) runs in DEBUG/TRACE mode, printing every command as it's actually executed, with variables already substituted — genuinely invaluable when a script isn't doing what you expect and you need to see exactly what it's really running, rather than guessing from the script's source alone.
In your stack
- →
A real deployment script should use
set -euo pipefailso that ifmvn packagefails, the script stops immediately rather than proceeding to try running a JAR that was never successfully built — a genuinely important safeguard, since silently attempting to run a stale or missing JAR after a failed build is a classic, confusing failure mode.
Code & diagrams
The exact same script, with and without set -e, after step 2 fails.
The standard defensive header for any real script, plus a specific error-message pattern.
#!/usr/bin/env bash
set -euo pipefail
# -e: exit immediately on any command failure
# -u: exit if an unset variable is used
# -o pipefail: a pipeline fails if ANY stage fails, not just the last one
echo "Building application..."
if ! mvn clean package; then
echo "ERROR: build failed — aborting deployment" >&2
exit 1
fi
echo "Build succeeded, deploying..."
JAR_FILE=$(ls target/*.jar 2>/dev/null) || {
echo "ERROR: no JAR file found after build" >&2
exit 1
}
echo "Deploying $JAR_FILE"
java -jar "$JAR_FILE"Watching exactly what a script does, step by step.
# Run any script in trace mode to see every command as it executes
bash -x deploy.sh
# Or add this line temporarily inside the script itself, near the top
# set -x
# Turn tracing back off partway through a script, if only part of it is suspect
# set +xExplain it without notes
Why does a script without set -e sometimes fail in a genuinely confusing way, even though one of its early commands clearly failed?
What specific problem does pipefail solve that set -e alone does not?
Practice
Take any script you've already written in this phase and add set -euo pipefail to the top, then deliberately introduce a typo'd variable name and confirm the script now fails immediately with a clear error instead of silently continuing.
Run any script you have in debug mode with bash -x and read through the trace output line by line, matching each traced line back to the actual source line that produced it.
Trade-offs
- ↔
set -euo pipefailis close to a strict improvement for most scripts, but it can occasionally be too aggressive — a command you EXPECT might fail sometimes (as part of normal, handled logic, like checking if a process exists before deciding whether to kill it) needs to be explicitly wrapped (command || true, or an explicit if-check) so its expected failure doesn't trigger the whole script to abort unnecessarily.
Done when you can
I understand why set -e is important and use it as a standard header in my scripts.
I know what set -u and pipefail each specifically catch, and why neither alone is sufficient.
I can debug a misbehaving script using bash -x to see exactly what it's actually executing.