set -euo pipefail at the top of a script has become a near-universal reflex, copied from one script to the next without always understanding what each of the three flags actually catches, and more importantly, what they never catch. This false sense of security explains why a script starting with this line sometimes keeps running after an error you thought was covered.
-e: stop on error, with documented but forgotten exceptions
set -e stops the script the moment a command returns a non-zero exit code, but this rule has precise exceptions bash explicitly documents: a command inside a condition (if, while, until), inside an && or || chain, or negated with !, never triggers the stop, whatever its exit code.
set -e
if grep -q "pattern" file.txt; then
echo "found"
fi
# grep failing (pattern absent) never stops the script:
# it's inside an if condition
This exception is deliberate and documented: without it, testing a command inside an if would become impossible, since that test’s expected failure would systematically stop the script.
-u: undefined variables turned into errors
set -u turns using a never-defined variable into an explicit error rather than a silent substitution with an empty string, a bash default that hides typos ($FOOO instead of $FOO) for years without ever flagging them.
set -u
echo "$UNDEFINED_VAR"
# bash: UNDEFINED_VAR: unbound variable
# Without -u, this line would just print an empty line
This flag reveals an entire class of script bugs that, without it, produce silently incorrect behavior (an empty variable treated as valid) rather than an explicit error in the right place.
pipefail: a pipeline’s real exit code
Without pipefail, a pipeline’s (cmd1 | cmd2) exit code is always the last command’s, cmd2, regardless of cmd1 failing. A grep failing at the start of a pipeline whose last command succeeds makes the whole pipeline look successful.
set -o pipefail
false | echo "always printed"
# Without pipefail: exit code 0 (echo's)
# With pipefail: exit code 1 (false's, the failing command)
pipefail captures the exit code of the first command that fails in the pipeline, not just the last one, an essential correction for any script chaining commands via pipes and then checking overall success.
The trap that survives even with all three flags active
A function called inside a condition (if my_function; then) disables set -e for its entire execution, including for internal commands that have nothing to do with the tested condition. An error occurring inside that function, on a line unrelated to the intended conditional test, passes silently unnoticed, the exact same mechanism as set -e’s exception in a plain if, but far less visible once hidden behind a function call.
set -euo pipefail
my_function() {
rm important_file # fails if the file doesn't exist
echo "printed anyway despite rm failing above"
}
if my_function; then
echo "success"
fi
Takeaway
set -e never stops a script inside an if, an &&/|| chain, or a function called in conditional context, exceptions that are documented but rarely front of mind when writing the script. -u reveals variable typos as explicit errors rather than silent substitutions; pipefail captures the exit code of a pipeline’s first failing command, not just the last one. These three flags genuinely reduce a class of silent failures, but never replace an explicit exit-code check at a script’s critical points, especially anything running inside a function or a condition, one of the habits separating a genuinely reliable script from one that merely has the right line at the top, a detail that matters at every step of a CI/CD industrialization relying on shell scripts.