Lesson 20 / 24
Error Handling
Strict mode options, the scope and exceptions of early exit, undefined variable checking, pipeline exit codes, and the error-catching hook.
Contents
The script produces correct output, but only when everything goes right. When a command fails, the report is left half-finished, and the script’s exit code does not report it. An undefined variable silently expands to an empty string. An error in the middle of a pipeline stays invisible.
The shell’s default behavior is tolerant of failure: even if a command fails, the next one still runs. This is the right choice for interactive use — a mistyped command should not close the session. In a script, it works the other way.
Strict mode is a set of options that narrows the shell’s tolerance. This lesson takes up the options one by one, showing what each catches and — more importantly — what it does not catch.
Early Exit
The set -e option terminates the shell after a command whose exit code is
nonzero.
#!/usr/bin/env bash set -e echo "1: start" false echo "2: THIS LINE IS NOT PRINTED"
1: start
echo $?
1
The script stopped before reaching the second line and carried the failure code outward. The gain here is not just convenience: a script producing half its output and reporting success misleads every system that calls it.
Early Exit’s Exceptions
set -e does not treat a command’s failure as fatal everywhere. The exceptions
make sense, but there are many of them, and not knowing them gives a false sense
of security.
#!/usr/bin/env bash set -e if false; then echo "a"; fi echo "1: failure in a condition does not stop the script" false || echo "2: failure on the left of || does not stop it" false && echo "not reached" echo "3: failure on the left of && does not stop it either" false | true echo "4: does not stop if the pipeline's last command succeeds" ! false echo "5: a negated command does not stop it" f() { false; echo "f continued"; } if f; then :; fi echo "6: set -e is suspended inside a function called in a condition"
1: failure in a condition does not stop the script 2: failure on the left of || does not stop it 3: failure on the left of && does not stop it either 4: does not stop if the pipeline's last command succeeds 5: a negated command does not stop it f continued 6: set -e is suspended inside a function called in a condition
The first five of the six exceptions are by design: the failure of a command used as a condition carries information, it is not an error. The sixth is the most dangerous — early exit is suspended for the entire body of a function called within a condition, and real errors inside that body become invisible.
For this reason, set -e alone is not a guarantee. Critical commands’ exit
codes must be checked separately, or an error hook must be set up.
Undefined Variable Checking
The set -u option treats the expansion of an undefined variable as an error.
#!/usr/bin/env bash set -u echo "log: ${LOG}"
./u.sh: line 3: LOG: unbound variable
This option catches the most destructive class of error in shell scripts. The
line rm -rf "$DIR/" becomes rm -rf / when DIR is undefined — with the
option active, the script never reaches that line at all.
The option makes using default-value notation mandatory:
#!/usr/bin/env bash set -u echo "log: ${LOG:-access.log}" echo "first argument: ${1-none}" echo "argument count: $#"
log: access.log first argument: none argument count: 0
The ${v-fallback} and ${v:-fallback} notations introduced in the Variables
and Quoting Rules lesson turn into a requirement here: if an unset positional
parameter is going to be read, a default value must be written. This also
produces a side effect that documents the script’s interface.
Pipeline Exit Code
set -o pipefail sets a pipeline’s exit code equal to the code of the
rightmost command that returns a nonzero code. It was introduced in the
Pipelines lesson; it is an inseparable part of strict mode.
#!/usr/bin/env bash set -eo pipefail grep '999' access.log | wc -l echo "THIS LINE IS NOT PRINTED"
0
echo $?
1
Without pipefail, this script would print 0 and finish successfully: wc
ran without a problem, and the pipeline’s code was its code.
With pipefail and set -e together, it must be kept in mind that pipelines
cut off by SIGPIPE will also count as failures. Pipelines in the form
command | head -5 must be suppressed deliberately.
Narrowing the Field Separator
The assignment IFS=$'\n\t' removes the space from word splitting: splitting
happens only on newline and tab.
IFS=$'\n\t' d="a b:c" printf '<%s>\n' $d
<a b:c>
Even the unquoted expansion did not split. This is an extra defense against the class of errors caused by file names containing spaces.
A warning is needed: this setting is global and affects every piece of code that relies on splitting by space. A loop that wants to split a variable into its words runs for a single round after this setting. While strict mode’s other three options are recommended unconditionally, this one has to be decided by looking at what the script does.
The Arithmetic Trap
The Arithmetic and Command Substitution lesson showed that the (( ))
command’s exit code is the inverse of the value. Under strict mode this
turns into an error that stops the script:
#!/usr/bin/env bash set -e counter=0 (( counter++ )) echo "THIS LINE IS NOT PRINTED (counter=$counter)"
echo $?
1
The postfix form counter++ returned the old value — that is, zero — and
(( )) counted this as a failure. The safe form is arithmetic expansion:
#!/usr/bin/env bash set -e counter=0 counter=$(( counter + 1 )) echo "counter=$counter"
counter=1
This trap shows why strict mode is applied carefully: the options do not change the script’s meaning, but they change which situations count as an “error,” and some idioms are incompatible with this new definition.
Suppressing Expected Failures
It is normal for some commands to return a nonzero code; a filter tool’s “no match” report is the typical example. Under strict mode, these situations are suppressed explicitly.
#!/usr/bin/env bash set -euo pipefail count=$(grep -c 'NOMATCH' access.log || true) echo "match count: $count" if grep -q 'NOMATCH' access.log; then echo "found"; else echo "not found"; fi
match count: 0 not found
Two methods were used. || true zeroes the code unconditionally; it suits
cases where the number itself is meaningful. Calling within an if, on the
other hand, already interprets the failure as a condition; this is the correct
form for an existence question.
|| true must be used with restraint. Added to every line, strict mode loses
its meaning, and the script reverts to its old form that swallows errors. The
rule: suppression is done knowing why that command’s failure is expected,
and documented with a comment when necessary.
The Error Hook
trap ... ERR runs a handler for every command that triggers early exit.
Reporting where the error occurred makes the silent exits strict mode produces
traceable.
The set -E option makes this hook valid inside functions and subshells too;
without it, the hook only runs at the top level.
#!/usr/bin/env bash set -Eeuo pipefail IFS=$'\n\t' handle_error() { local code=$? line=$1 printf 'report.sh: error at line %d, exit code %d\n' "$line" "$code" >&2 exit "$code" } trap 'handle_error "$LINENO"' ERR echo "1: start" grep -q 'NOMATCH' access.log echo "2: THIS LINE IS NOT PRINTED"
1: start report.sh: error at line 13, exit code 1
The first line in the hook’s body is written as local code=$?; $? must be
read before any other command runs, or its value changes. $LINENO is left
inside single quotes so that it expands where the hook is triggered, not
where it is set up.
The trap command itself and signal handling are the next lesson’s subject.
The Strict Mode Header
This lesson’s result is a fixed header placed at the top of scripts:
#!/usr/bin/env bash set -Eeuo pipefail
Four options close four separate error classes: a failed command, an undefined
variable, a code lost in a pipeline, and an error hook lost inside a function.
The fifth — narrowing IFS — is decided on a per-script basis.
The header is not a guarantee, it is a baseline. Keeping set -e‘s exceptions
in mind, critical commands’ codes still have to be checked by hand. Strict
mode catches what gets overlooked; it does not stand in for what should not be
overlooked.
Summary
set -eterminates the shell after a command that returns a nonzero code; it is suspended in condition contexts, on the left of chain operators, and inside the body of functions called in a condition.set -utreats undefined variable expansion as an error and makes default-value notation mandatory.set -o pipefailties a pipeline’s exit code to the code of its first failing link.(( counter++ ))reports failure at a value of zero; under strict mode,counter=$(( counter + 1 ))is written instead.- Expected failures are suppressed deliberately, with
|| trueor a condition context. trap ... ERR, together withset -E, sets up a hook that reports the error’s line number.
Next Step
Strict mode stops the script the moment there is an error — but what does it leave behind at the point where it stops? A half-written output file, uncleaned temporary files, a lock another process is waiting on. The next lesson takes up signals and cleanup hooks that run on exit; the safe creation of a temporary file and its deletion on every exit path will also be set up there.
To keep your progress and take notes, Log in
My notes
Log in to take notes.