Bash Strict Mode Pitfalls With Set E and Pipefail in Production Scripts
Quick answer
Bash strict mode, usually written as set -euo pipefail, makes scripts stop on errors, unset variables, and failed pipeline stages. It catches many mistakes,...
Bash strict mode, usually written as set -euo pipefail, makes scripts stop on errors, unset variables, and failed pipeline stages. It catches many mistakes, but set -e has surprising exceptions and is not a complete error handling strategy. In production scripts, combine strict mode with explicit checks, a trap for cleanup, and careful handling of commands that are allowed to fail.
What Do set -e, set -u, and pipefail Actually Do?
Each option addresses a different failure mode. Together they are often called unofficial strict mode, but they are separate features with separate quirks.
- set -e: exit immediately if a command returns a non-zero status, with several important exceptions.
- set -u: treat the use of an unset variable as an error instead of silently expanding to an empty string.
- set -o pipefail: make a pipeline return the status of the last command that failed, not just the final command.
The unset variable check prevents a classic disaster. A script that runs rm -rf with a variable that turned out empty can delete the wrong directory. With set -u, that mistake aborts the script instead.
#!/usr/bin/env bash
set -euo pipefail
backup_dir="${BACKUP_DIR:?BACKUP_DIR is required}"
rm -rf -- "${backup_dir:?}/old"
Where Does set -e Surprise You?
The most important thing to know is that set -e is not applied everywhere. Bash deliberately ignores failures in certain contexts, because those commands are being used as tests.
A failing command does not trigger exit when it is part of an if condition, a while or until condition, part of a list joined with && or || (except the last command), or when its status is being negated with an exclamation mark. This means a failure inside a function called from an if statement can be silently ignored, even though the same function would abort the script when called directly.
Another surprise involves command substitution and local. In a line such as local value=$(some_command), the exit status you see is that of local, which succeeds, so a failure in the substitution is masked. Declare the variable first and assign on a separate line to catch the error.
Behavior also differs between shells and versions in some edge cases, so consult the Bash manual for details when a script depends on them.
How Does pipefail Change Pipelines?
Without pipefail, a pipeline succeeds if its last command succeeds, even if an earlier stage failed. A command like curl fetching a file and piping it into a parser would appear to work when the download failed but the parser accepted empty input.
With pipefail enabled, the pipeline fails if any stage fails. There is a catch: some commands exit non-zero in normal situations. For example, grep returns 1 when it finds no match, which is not an error in many scripts. Under strict mode, that would abort the script, so handle it explicitly.
count=$(grep -c "ERROR" app.log || true)
if ! grep -q "ready" status.txt; then
echo "service not ready" >&2
exit 1
fi
Also be aware of SIGPIPE. When a downstream command such as head closes early, the upstream command may be killed with a broken pipe, which pipefail then reports as a failure.
What Is a More Reliable Error Handling Approach?
Treat strict mode as a safety net, not the whole plan. Check critical commands explicitly and give clear error messages, so failures are easy to diagnose.
- Use trap with ERR and EXIT to log failures and clean up temporary resources.
- Check required commands and variables at the top of the script.
- Use explicit if checks or || exit for operations where failure needs a specific response.
- Quote variables and use -- before filenames to avoid word splitting and option injection.
- Run ShellCheck in CI to catch common mistakes before they reach production.
Frequently Asked Questions
Should every Bash script use set -euo pipefail?
It is a good default for automation scripts, but understand its limits. For interactive or highly exploratory scripts, it can get in the way.
Why did my script continue after a failed command?
The command was probably in a context where set -e is ignored, such as an if condition, an AND-OR list, or a function called in those contexts.
How do I allow a command to fail?
Append || true, or test it in an if statement. Add a comment explaining why the failure is acceptable.
Does set -u break optional variables?
It errors on unset ones. Use default expansion such as ${VAR:-default} when a variable is allowed to be missing.
Summary
Strict mode improves safety but has real gaps. Combine set -euo pipefail with explicit checks, traps, quoting, and ShellCheck. Consult the Bash manual for exact behavior in your version.