Skip to content
academia.sh

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 -e terminates 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 -u treats undefined variable expansion as an error and makes default-value notation mandatory.
  • set -o pipefail ties 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 || true or a condition context.
  • trap ... ERR, together with set -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.

Start typing to search.

↑↓ Esc navigate · open · close