Chapter 11Lesson 01~90 minutes

set -e, errexit, and Its Surprising Edge Cases

Bash's `set -e` is often described as “exit on error,” but that description is incomplete. Its behavior depends on shell grammar and on whether a non-zero status is being used as control flow.

IntermediateError handlingHands-on lab

Learning objectives

By the end of this lesson

  • Explain what errexit actually does.
  • Recognize conditional and AND/OR exemptions.
  • Understand function and arithmetic edge cases.
  • Treat expected failure as explicit control flow.
  • Use errexit as a guardrail rather than exception handling.

1. errexit is a control-flow option, not exception handling

set -e (or set -o errexit) asks Bash to exit when an unhandled simple command or compound command returns non-zero. The phrase “unhandled” is the source of most surprises.

Simplified errexit decision
flowchart TD
  C["command returns non-zero"] --> Q{"status is being tested?"}
  Q -->|"yes"| K["continue; caller handles it"]
  Q -->|"no"| E["errexit may terminate shell"]
Important limitation

set -e does not turn Bash into a language with exceptions. Its behavior depends on shell grammar and context.

2. The simplest failure can terminate the script

#!/usr/bin/env bash
set -e

printf 'before\n'
false
printf 'after\n'  # not reached

This is the intuitive case: false returns status 1 and is not part of a condition that expects a non-zero result.

3. Commands tested by if are exempt

set -e

if grep -q '^READY$' status.txt; then
  printf 'ready\n'
else
  printf 'not ready or grep failed\n'
fi

printf 'script continues\n'

Bash assumes a command in an if condition is being intentionally tested, so errexit does not terminate the shell merely because the condition is false.

4. && and || lists also change errexit behavior

set -e

check_config && deploy
check_optional || printf 'optional check failed\n' >&2

Commands whose status controls && or || are part of explicit control flow. Do not assume every non-zero status inside these lists aborts the shell.

5. while and until conditions expect non-zero statuses

set -e

attempt=0
while (( attempt < 3 )); do
  ((attempt += 1))
  if probe_service; then
    break
  fi
  sleep 1
done

Loop conditions are another grammar context where non-zero status is expected and therefore does not behave like an unhandled failure.

6. Functions inherit the calling context

set -e

do_work() {
  false
  printf 'function continued\n'
}

if do_work; then
  printf 'reported success\n'
fi
Surprising edge case

When a function is called in a context that tests its status, errexit behavior inside the function may be suppressed in ways that surprise readers. Prefer explicit status checks inside critical functions.

7. Subshell and command-substitution behavior needs testing

set -e

(
  printf 'subshell start\n'
  false
  printf 'subshell after failure\n'
)

Option inheritance and command-substitution behavior can differ by Bash version and settings such as inherit_errexit. Do not base production correctness on assumptions you have not tested on supported Bash versions.

8. Arithmetic commands can return non-zero for ordinary values

set -e

count=0
# This can return status 1 because the arithmetic result is 0:
((count++))

printf 'count=%d\n' "$count"
Classic pitfall

Arithmetic commands use zero/non-zero numeric result to determine shell status. Use forms such as ((count += 1)) carefully and understand their returned status in strict mode.

9. Expected failure should be written as expected control flow

if ! validate_config "$file"; then
  printf 'invalid config: %s\n' "$file" >&2
  exit 65
fi

The point is not to “defeat” set -e; it is to make expected non-zero statuses visible in the program structure.

10. Capture status around operations whose failure needs explanation

if deploy_service "$service"; then
  printf 'deploy succeeded\n'
else
  status=$?
  printf 'deploy failed service=%s status=%d\n' \
    "$service" "$status" >&2
  exit "$status"
fi

Explicit branches produce better diagnostics than relying on a shell-wide implicit exit.

11. Hands-on lab: observe errexit contexts

mkdir -p "$HOME/devops-academy/bash/chapter11/lesson01"
cd "$HOME/devops-academy/bash/chapter11/lesson01"

cat > demo.sh <<'EOF'
#!/usr/bin/env bash
set -e

printf '1: ordinary success\n'
true

printf '2: failure inside if condition\n'
if false; then
  printf 'unexpected true\n'
else
  printf 'condition was false; script continues\n'
fi

printf '3: handled failure\n'
if bash -c 'exit 7'; then
  :
else
  status=$?
  printf 'captured status=%d\n' "$status"
fi

printf '4: done\n'
EOF

bash demo.sh

Verification checklist

12. Knowledge check

Question 1. Does set -e exit on every non-zero command?

Question 2. Why can functions be surprising under errexit?

Question 3. Why can arithmetic commands trip errexit?

Question 4. What is the safest way to handle an expected failure?

13. Summary

set -e is useful as a guardrail, not as a complete error-handling model. Understand its grammar-dependent exemptions, make expected failures explicit, and use direct status checks where diagnostics or recovery matter.

14. Further reading

  • GNU Bash Reference Manual — The Set Builtin and errexit.
  • GNU Bash Reference Manual — Bourne Shell Builtins.
  • BashFAQ/105 — discussion of set -e pitfalls.
  • ShellCheck documentation on errexit-related patterns.
Next lesson

set -u, nounset, and Safer Variable Access

Continue Chapter 11 by making Bash failure behavior more explicit and predictable.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.