Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~15 min· topic 4 of 4

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.

0/4 · 0%

Key ideas

  1. 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.

  2. 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.

  3. 03

    set -u makes the script exit immediately if it tries to use a variable that was NEVER SET — catching typos in variable names ($NAM instead of $NAME) immediately, rather than silently treating the mistyped variable as an empty string and continuing with wrong, unexpected behavior further down the script.

  4. 04

    set -euo pipefail (a genuinely standard, widely-recommended combination) adds one more piece: without pipefail, a pipeline's exit code is only the LAST command's — meaning cat missing-file.txt | grep foo could report success (grep's exit code) even though cat itself clearly failed. pipefail makes the whole pipeline fail if ANY command within it fails, not just the last one.

  5. 05

    Explicitly checking $? (a command's exit code, from Phase 3) after anything genuinely critical, even with set -e already 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.

  6. 06

    bash -x script.sh (or adding set -x inside 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 pipefail so that if mvn package fails, 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

SetEEffectdiagram

The exact same script, with and without set -e, after step 2 fails.

Rendering diagram…
error-handling.shbash

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"
debug-mode.shmarkdown

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 +x

Explain it without notes

01

Why does a script without set -e sometimes fail in a genuinely confusing way, even though one of its early commands clearly failed?

02

What specific problem does pipefail solve that set -e alone does not?

Practice

01

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.

02

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 pipefail is 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.