| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Bash strict mode semantics: set -e, set -u, pipefail, errexit exceptions, inherit_errexit, SIGPIPE, functions, positional parameters, and a strict-mode template. Does not cover full trap/ERR handling or set -E errtrace details. |
| Related guides | Shell scripting course hub |
set -euo pipefail enables three independent Bash safeguards: exit on many unhandled command failures, reject unset variables, and make a pipeline return failure when one of its commands fails. Bash does not have an official "strict mode," though, and each option has exceptions worth understanding before you add the line to every script.
pipefail is not part of portable POSIX sh. Use a Bash shebang (#!/usr/bin/env bash or #!/bin/bash) when your script depends on Bash features such as pipefail and shopt.
What Does set -euo pipefail Mean?
There is no strict_mode switch in Bash. Blog posts and style guides lump three independent options together:
| Option | Short form | What it changes |
|---|---|---|
| errexit | set -e |
Exit when an eligible command returns non-zero |
| nounset | set -u |
Treat expansion of unset variables as an error |
| pipefail | set -o pipefail |
Pipeline exit status reflects the rightmost failing command |
The combined form is shorthand for three separate settings:
set -euo pipefailexpands to:
set -e # errexit on
set -u # nounset on
set -o pipefail # pipefail onUse + instead of - to turn an option off: set +e, set +u, and set +o pipefail.
That preamble is a useful baseline for many scripts, not a guarantee of production-grade error handling. The sections below cover where each flag actually bites and where it quietly does nothing.
set -e: Exit on Unhandled Command Failures
set -e (errexit) tells Bash to exit when an eligible command or compound command returns a non-zero status. The important word is eligible: Bash deliberately ignores non-zero statuses in several control-flow contexts.
Failure means any exit status other than zero. The Bash exit codes article covers how $? records that status; errexit acts on the status of the command that just finished, not on a later line.
With errexit enabled, a bare failing command stops the script:
set -e
false
echo "not reached"The echo never runs and the shell exits with status 1.
Where set -e Does Not Exit
Errexit is not "exit on every non-zero status." Treating it like try/except in another language causes most strict-mode surprises.
| Context | Errexit behavior |
|---|---|
Command in if, while, or until test |
Non-zero status does not exit |
Command in && or || list |
Exempt except the command after the final && or || |
Command after ! |
Negated test; non-zero may be expected |
| Command in a pipeline, except the last | Its individual non-zero status does not trigger errexit; pipefail can make the pipeline itself return non-zero |
After set +e |
Disabled until set -e runs again |
A grep -q miss returns status 1, which is normal control flow inside a conditional:
set -e
if grep -q '^missing$' /etc/hosts; then
echo "found"
else
echo "no match — script continues"
fiThe same grep as a standalone command aborts the script:
set -e
grep -q '^missing$' /etc/hosts
echo "not reached"Commands in && and || lists are exempt from errexit except the command that follows the final && or || in that list:
set -e
false && echo "skipped"
echo "after &&"
true || echo "skipped"
if ! false; then echo "negation works"; fifalse on the left of && does not exit because Bash is evaluating short-circuit logic, not treating the failure as fatal.
A while or until test uses the same exemption. When grep finds nothing, the loop ends and the script continues:
set -e
while grep -q 'NOTFOUND' /etc/hosts; do
echo "loop body"
done
echo "grep returned non-zero, script continues"The ((count++)) Trap
Another classic errexit surprise is post-increment in arithmetic:
set -e
count=0
((count++))
echo "not reached"((count++)) evaluates to the old value (0). Bash arithmetic commands return status 1 when the expression evaluates to zero. Under set -e, that can terminate the script even though the increment itself succeeded.
For a counter starting at zero, ((++count)) avoids this particular first-increment failure. If the arithmetic result can legitimately be zero, use an assignment such as count=$((count + 1)) when you do not want the arithmetic value to determine command success.
set -u: Catch Unset Variables
set -u (nounset) turns unset parameter expansions into errors. That catches typos like $LOG_DIR when you meant $LOGDIR.
Optional Variables and Defaults
Optional values need a safe default or an explicit check. Bash parameter expansion provides forms that work under nounset:
set -u
echo "${OPTIONAL_VAR:-default-value}"The :- operator supplies a default without referencing an unset name bare.
A bare unset reference still fails:
set -u
echo "$UNSET_VAR"bash: UNSET_VAR: unbound variableMissing Positional Parameters ($1, $2)
A common real-world nounset failure is reading $1 when no argument was passed:
set -u
name=$1Running that script without arguments triggers an unbound-variable error on $1.
Require arguments explicitly:
set -u
if (( $# < 1 )); then
echo "Usage: $0 NAME" >&2
exit 2
fi
name=$1For optional arguments, use a default:
set -u
name=${1:-default}set -o pipefail: Catch Pipeline Failures
By default, a pipeline's exit status is the status of the last command, even when an earlier stage failed. The Bash pipes guide covers PIPESTATUS and default pipeline rules; pipefail changes which status $? reports.
Without pipefail, the last command can mask an earlier failure:
set -eu
false | true
echo "reached — errexit did not fire"With pipefail and errexit together, the pipeline status is non-zero and the script stops:
set -euo pipefail
false | true
echo "not reached"The cmd | head SIGPIPE Trap
pipefail can turn an intentional early-close pipeline into a non-zero result because the producer receives SIGPIPE when the consumer stops reading. When you need per-stage status after a pipeline, read ${PIPESTATUS[*]} immediately; $? only keeps the overall pipeline status.
A producer that writes indefinitely makes the effect obvious:
set -euo pipefail
set +e
yes | head -n 1 >/dev/null
printf 'pipeline=%d PIPESTATUS=(%s)\n' "$?" "${PIPESTATUS[*]}"
set -e
yes | head -n 1 >/dev/null
echo "not reached under errexit"pipeline=141 PIPESTATUS=(141 0)Status 141 is 128 + 13 (SIGPIPE). With pipefail and set -e restored, the second pipeline aborts the script before the final echo.
When an early-closing consumer such as head is normal for your pipeline, handle it in an if test, inspect ${PIPESTATUS[*]}, or temporarily disable errexit for that one line.
Command Substitution and inherit_errexit
In Bash, command substitution $(...) normally runs with errexit disabled inside the subshell unless inherit_errexit is on. A failing command inside $() can continue to the next line in that subshell instead of stopping it.
See Bash command substitution for the general $() model.
Save this reproducible script as /tmp/bash-strict-mode-lab/inherit-errexit.sh to compare both modes in one run:
mkdir -p /tmp/bash-strict-mode-labWrite, mark executable, and run a script that toggles inherit_errexit between two $(demo) calls:
cat > /tmp/bash-strict-mode-lab/inherit-errexit.sh << 'EOF'
#!/usr/bin/env bash
set -e
demo() {
false
echo "continued inside command substitution"
}
echo "=== without inherit_errexit ==="
shopt -u inherit_errexit 2>/dev/null || true
result=$(demo)
printf 'result: %s\n' "$result"
echo "=== with inherit_errexit ==="
shopt -s inherit_errexit
result=$(demo)
echo "this line is not reached"
EOF
chmod +x /tmp/bash-strict-mode-lab/inherit-errexit.sh
/tmp/bash-strict-mode-lab/inherit-errexit.sh; echo "exit: $?"Sample output:
=== without inherit_errexit ===
result: continued inside command substitution
=== with inherit_errexit ===
exit: 1Without inherit_errexit, false does not terminate the substitution, so echo runs and the function succeeds. With inherit_errexit, execution stops at false inside $().
This does not mean every failed command substitution is hidden from the parent shell. For example, result=$(false) returns the status of false, so a parent shell running with set -e can still exit. inherit_errexit matters when multiple commands run inside $(): it determines whether an earlier failure stops the substitution or execution continues to a later command that may return success.
Enable inherit_errexit when you intentionally want failures inside command substitutions to follow the caller's errexit setting.
Don't combine local with a command substitution you need to check
local, declare, and export can mask the exit status of a command substitution because the builtin's successful status becomes the status of the command:
set -e
myfunc() {
local result=$(false)
echo "still running"
}
myfunc
echo "after myfunc"myfunc completes and the caller continues even though false ran inside $().
Split the assignment:
set -e
myfunc() {
local result
result=$(false)
echo "still running"
}
myfuncNow errexit stops at result=$(false).
Functions Can Change set -e Behavior
When a function is invoked as the test of an if, errexit is suspended inside the function body. A failing command can fall through, and the function's return status may come from a later successful command:
set -e
run_check() {
grep -q '^missing$' /etc/hosts
echo "inside function after grep"
}
if run_check; then
echo "function returned 0"
else
echo "function returned non-zero"
fiinside function after grep
function returned 0grep failed, but echo returned 0, so the if saw success. If the command's failure must determine the function result, check it explicitly instead of relying on set -e inside a function used as a conditional:
run_check() {
grep -q '^missing$' /etc/hosts || return
echo "inside function after grep"
}Or branch on the result:
run_check() {
if ! grep -q '^missing$' /etc/hosts; then
return 1
fi
echo "check succeeded"
}set -e inside a function is not function-local. Shell options changed inside a function remain changed after it returns because functions execute in the current shell context.
Sourced files (source or .) run in the current shell too. The Bash source vs execute article contrasts sourcing with running a script in a child process. A sourced library should generally avoid changing the caller's shell options. If it must change them, document that behavior or save and restore the previous option state.
How to Handle Expected Failures
When a non-zero status is expected, structure the command as control flow instead of turning errexit off for a whole block.
The clearest pattern is if or if !. The Bash if, elif, else lesson uses the same branching style:
set -e
if ! grep -q '^missing$' /etc/hosts; then
echo "expected miss"
fi
echo "script continues"if ! cmd is useful when any non-zero status means the same thing to your script. If different non-zero statuses have different meanings, preserve and inspect the exit status instead. For grep, status 1 means no match, but status 2 (or higher) means an actual error such as a missing file; if ! grep puts both outcomes in the same branch.
|| true intentionally discards every non-zero status. if ! cmd makes the expected failure branch explicit, but use an ordinary if and inspect $? when different failure statuses need different handling.
A set +e / set -e pair around one command also works, yet it is easier to misread than an if test. Reserve broad set +e regions for legacy code you are refactoring.
What Strict Mode Still Does Not Catch
set -euo pipefail is not comprehensive error propagation. Explicit checks still matter for expected failures, cleanup, and operator-facing messages.
Failures inside process substitution (<(...)) are not automatically propagated to the parent command:
set -euo pipefail
cat <(false)
echo "after cat — errexit did not fire"false failed in the process substitution, but cat read empty input and returned 0. The parent script continues.
Errexit is a safety net for commands you truly expect to succeed. It does not replace documenting which failures are normal or checking statuses you care about.
Should Strict Mode Change IFS?
Some older strict-mode recipes also set IFS=$'\n\t'. That is not part of set -euo pipefail and changes how Bash performs word splitting.
Prefer proper quoting and arrays. Change IFS only when your script intentionally needs different field splitting, ideally in the smallest possible scope.
Recommended Bash Strict-Mode Template
A useful starting header:
#!/usr/bin/env bash
set -euo pipefail
# Optional on Bash 4.4+: make command substitutions inherit -e
shopt -s inherit_errexitinherit_errexit was added in Bash 4.4. Add that line only when you need command substitutions to honor errexit; Bash 4.3 and earlier keep their normal $() behavior without it.
Pair the template with quoted expansions, if ! for expected failures, and trap cleanup on EXIT when scripts create temp files. set -E (errtrace) is separate from set -e; it controls whether ERR traps inherit into functions and subshells.
Common Bash Strict-Mode Pitfalls
| Code / pattern | Surprise |
|---|---|
if grep -q … |
set -e does not exit on non-zero test status |
grep -q … (bare) |
Non-zero match status aborts the script |
cmd1 | cmd2 without pipefail |
Last stage can hide earlier failure |
yes | head (with pipefail) |
SIGPIPE (141) can abort under set -e |
((count++)) from zero |
Arithmetic status 1 can exit under set -e |
local x=$(cmd) |
local can mask cmd failure status |
x=$(cmd) |
Default: errexit off inside substitution |
$1 with set -u |
Missing argument is an unbound variable |
cat <(cmd) |
Failure inside <(...) may not fail the parent |
Function called from if |
Errexit suppression extends into function body |
set -e inside a function |
Option persists after the function returns |
The fundamentals are simple; most strict-mode problems come from these exceptions.
Summary
Bash strict mode is a label for set -e, set -u, and pipefail used together. Errexit exits on non-zero statuses only for eligible commands, with well-documented exemptions in conditionals, short-circuit lists, and negated tests. Nounset catches unset names and missing positional parameters. pipefail stops the last pipeline stage from hiding an earlier failure, including SIGPIPE when an early consumer closes the pipe.
The surprises cluster around command substitution (inherit_errexit, local var=$(cmd)), arithmetic increment, functions called from if, and process substitution. Use the template when it fits, add inherit_errexit when $() must honor errexit, and reach for explicit if tests when failure is part of the design.
References
- Bash manual — The Set Builtin
- Bash manual — Shell Builtin Commands (
shopt) - BashFAQ/105 — Why doesn't
set -edo what I expected?
Frequently Asked Questions
Is set -euo pipefail always recommended?
No. It is a strong default for scripts where unexpected failure should stop execution, but it conflicts with commands that use non-zero status as normal control flow unless you wrap them in if, ||, or similar. Libraries sourced into interactive shells should not enable strict mode without warning.
Why did set -e not exit?
Bash suspends errexit for commands in if/while/until tests, for commands in &&/|| lists except the one after the final && or ||, and for negated commands with !. Arithmetic like ((count++)) when the expression evaluates to zero can also exit under errexit.
Why did grep stop my script?
grep returns 1 when there is no match. That is failure for errexit when grep runs as a standalone command. Use if grep -q …, if ! grep -q …, or grep … || true when no match is expected.
Does set -e work inside $()?
Not inside the substitution by default. Bash normally clears errexit in the subshell created for $(...), although the command substitution itself can still return a non-zero status to the parent. Bash 4.4+ provides shopt -s inherit_errexit, which makes the substitution inherit the caller's -e setting.
Should I add IFS=$'\n\t'?
Only when you have a specific word-splitting problem and understand the global side effect. Quoted variables and arrays solve most issues. Treat IFS changes as a deliberate technique, not part of the strict-mode bundle.

