Why `set -e` does not exit on failure
set -e has exceptions: commands in if, ||, &&, pipelines without pipefail, and functions called from conditionals never trigger an exit. The full list.
Error String
set -e is on but the script continues after a failure
Tldr
`set -e` is intentionally lenient: it ignores failures inside conditionals (`if`, `&&`, `||`, `!`), inside the test of a loop, and inside pipelines (only the last command's exit status counts unless `pipefail` is set). Add `set -o pipefail` and check command exit codes explicitly when you need strict failure semantics.
Cause
POSIX defines specific contexts where `set -e` is suppressed so common idioms like `if grep …; then` don't abort. People treat `set -e` as "fail on any error" but it really means "fail on unchecked top-level errors".
Repro
#!/usr/bin/env bash
set -e
false | true # no abort — last command (true) succeeded
if false; then :; fi # no abort — false is the condition
my_fn() { false; }
my_fn || echo "handled" # no abort — || handles the failureFix
#!/usr/bin/env bash
set -euo pipefail # add pipefail and nounset
# Check critical commands explicitly:
if ! critical_command; then
echo "failed" >&2
exit 1
fiExplanation
`set -euo pipefail` is the recommended "strict mode" baseline. For functions that should propagate failures, avoid calling them with `||` unless you genuinely want to handle the error.
Faq
Q
Does set -e work inside functions?
A
Yes, unless the function is invoked in a condition context — then failures inside it are suppressed too.
Q
Why is `local x=$(false)` not fatal?
A
`local` is a command that succeeds, and it becomes the statement exit status, hiding the substitution failure.
Deep Dive
Heading
The documented exceptions
Body
`set -e` ignores failures in a command that is part of a condition: anything in `if`, `while`, `until`, on the left of `&&` or `||`, or negated with `!`. A function called in such a context inherits the suppression for its whole body, which is the most surprising case — a helper that works standalone stops aborting when someone writes `if my_helper; then`. Failures inside `$(...)` used in an assignment are also ignored, because the assignment itself succeeds.
Heading
Making failures visible anyway
Body
Add `set -o pipefail` so a failing producer in a pipeline is not masked by a successful consumer, and check important commands explicitly: `cmd || { echo "cmd failed" >&2; exit 1; }`. Separate declaration from assignment — `local out; out=$(cmd)` — so the command status is the statement status. An `ERR` trap with `set -o errtrace` gives a stack trace on failure, and ShellCheck flags several of the patterns where `set -e` silently does nothing.
Checklist
Check whether the failing command sits in a condition or after `&&`/`||`.
Split `local var=$(cmd)` into declaration and assignment.
Add `set -o pipefail` for pipelines.
Use explicit `|| { ...; exit 1; }` for commands that must not fail.