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.
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.
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"]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 reachedThis 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' >&2Commands 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
doneLoop 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'
fiWhen 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"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
fiThe 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"
fiExplicit 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.shVerification 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 -epitfalls. - ShellCheck documentation on errexit-related patterns.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.