SkillByAIOpen interactive version →

Lesson 16 / 25

Strict Mode: set -euo pipefail and Its Caveats

Make scripts fail fast and understand where errexit does not help.

Fail early instead of carrying on broken

By default Bash keeps running after a command fails, uses empty strings for unset variables and ignores failures inside pipelines, so a script can continue after a failed cd and delete files in the wrong directory. Many scripts start with set -euo pipefail: -e (errexit) exits when a command fails; -u (nounset) treats use of an unset variable as an error, catching typos; -o pipefail makes pipelines fail if any part fails. These help a lot, but -e has well-known caveats: it is ignored for commands in if conditions, in while/until conditions, on the left of && or ||, and in functions called from those contexts; a command substitution's failure inside local var=$(cmd) is masked by local's own success; and in arithmetic, (( count++ )) returns status 1 when the old value is 0, which can exit your script unexpectedly. So treat strict mode as a safety net, not a substitute for explicit checks of commands whose failure you need to handle.

Fail fast instead of cascading

Strict mode stops the script at the first unexpected failure instead of letting it carry on in a broken state.

Figure 6.1 — With and without strict mode.

Strict mode and its traps

Each comment marks a case where -e does not behave as people expect.

#!/usr/bin/env bash
set -euo pipefail

cd /srv/app                     # if this fails, the script exits (good)

local_example() {
    local version
    version=$(git describe --tags)   # separate declaration keeps the failure visible
    echo "$version"
}

if grep -q "ready" status.txt; then   # failure here is a condition, not an exit
    echo "ready"
fi

count=0
(( count++ )) || true           # (( 0 )) is "false": without || true, -e exits here
count=$(( count + 1 ))          # assignment form never returns a failing status

rm -f "${TMPDIR:?}/build-cache" # :? guards against an empty variable deleting the wrong path

Guard rm with :?

rm -rf "$dir/" with an empty $dir becomes rm -rf /. rm -rf "${dir:?}/" stops the script with an error if dir is unset or empty. Use it for every destructive command built from variables.

Quick check: Why can `local out=$(failing_command)` hide a failure even with set -e?

  • set -e is disabled inside functions
  • Command substitution always returns 0
  • The status of the line is that of the local builtin, which succeeds
  • local makes variables read-only
Answer

The status of the line is that of the local builtin, which succeeds — local returns its own success status, masking the command substitution's failure; declare and assign separately.