Linux Basic — set -e and set -u
set -e and set -u
Two of the three “strict mode” switches. set -e (a.k.a. errexit) makes the script exit on the first error. set -u (a.k.a. nounset) makes unset variables a fatal error. Together they catch most “silent failure” bugs — but each has surprises that bite the unprepared.
set -e — Exit on Error
#!/usr/bin/env bash
set -e
cp src.txt dst.txt # if this fails, the script exits here
chmod 600 dst.txt # this runs only if the cp succeeded
echo "done"
Without set -e, both commands run regardless of whether cp worked, and you’d silently have a missing destination file with a chmod 600 error you ignored.
Equivalent: set -o errexit and the shebang flag bash -e script.sh.
Where set -e Does NOT Trigger
This is the part everyone gets wrong. set -e ignores failure in these contexts:
1. Commands in conditions
set -e
if grep -q ERROR file.log; then … # grep returning 1 does NOT abort
fi
while read -r line; do … # read returning 1 (EOF) does NOT abort
done < file.txt
cmd || true # the `|| true` swallows the error
This is by design — you need to be able to test things without the script bailing.
2. Commands left of && / || / |
set -e
false && true # does NOT abort — false is being tested
false || echo "fine" # does NOT abort — false's failure is handled
false | true # does NOT abort by default — only the last command counts
3. Negated commands
set -e
! some_cmd # never aborts, regardless of some_cmd's exit code
4. Inside functions called in conditions
This is the famous bug:
set -e
my_func() {
false
echo "still here?" # YES — even though `false` failed
}
if my_func; then # function called in 'if' → set -e inside is suspended
echo "ok"
fi
When a function is called in a condition (or with || …, && …), set -e is disabled inside it. There’s no workaround at the function level; you must check the failure manually:
my_func() {
false || return $?
echo "won't reach here"
}
This is why many people consider set -e a half-solution. It catches the common cases but you still need explicit checks for the corners.
Inverting set -e — Per-Command Opt-Out
When you specifically want a command to be allowed to fail under set -e:
set -e
# Method 1 — || true
optional_thing || true
# Method 2 — || :
optional_thing || :
# Method 3 — disable just for this line
set +e
risky_command
rc=$?
set -e
# Method 4 — use 'if' (cleaner than +e/-e)
if ! risky_command; then
rc=$?
echo "expected failure, code $rc" >&2
fi
: (the no-op) and true are equivalent. The if form is the most readable when you also want to capture the exit code.
set -u — Error on Unset Variables
#!/usr/bin/env bash
set -u
name="Alice"
echo "Hello $nmae" # typo → script exits
# bash: nmae: unbound variable
This catches the classic typo bug that has bitten every shell programmer. With set -u, typos fail loudly instead of expanding to empty string.
Equivalent: set -o nounset.
Where set -u bites you
set -u
# Iterating an empty array (Bash < 4.4)
arr=()
for x in "${arr[@]}"; do echo "$x"; done
# bash: arr[@]: unbound variable ← in older Bash, "[@]" on empty array was an error
(Bash 4.4+ treats "${arr[@]}" on an empty array as zero words. Verify with bash --version if you support older systems.)
set -u
echo "$1" # if no argument passed, script exits
echo "${1:-default}" # safe — explicit default
Use the :- default for optional vars:
log_level=${LOG_LEVEL:-info}
out_dir=${1:-./build}
For checking whether a variable is set without triggering set -u:
[[ -v MAYBE ]] && echo "set" # Bash 4.2+
[[ -z "${MAYBE:-}" ]] && echo "empty or unset"
Strict-Mode Header — The Recipe
Most production scripts start with:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Breakdown:
-e— errexit-u— nounset-o pipefail— fail pipelines on any non-zero exit (next page)IFS=$'\n\t'— word-split only on newline/tab; defends against unquoted-variable surprises
This is sometimes called “unofficial Bash strict mode” and it’s a great starting point.
When not to use strict mode:
- One-shot interactive snippets (annoying)
- Scripts where exit codes are part of the contract (e.g. some checking tools intentionally exit 1 on “found mismatch”)
- The first 5 minutes of a script before you’ve defined a
diehelper
A Real-World Pattern — Strict But Friendly
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
die() { echo "ERROR: $*" >&2; exit 1; }
# Required env vars
: "${API_TOKEN:?API_TOKEN required}"
: "${REGION:?REGION required}"
# Optional with defaults
log_level=${LOG_LEVEL:-info}
dry_run=${DRY_RUN:-false}
main() {
local input=${1:-}
[[ -n "$input" ]] || die "Usage: $0 <input-file>"
[[ -r "$input" ]] || die "Cannot read $input"
process "$input" || die "processing failed"
}
main "$@"
Strict mode catches typos and silent failures; explicit checks with die provide friendly error messages.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
Relying on set -e to catch errors inside a function called in if |
check \|\| return inside the function |
set -e suspended in conditions |
Using bare $var with set -u for optional vars |
${var:-default} or ${var:?required} |
unbound triggers exit |
Wrapping every command in \|\| true “just in case” |
use set +e block or if |
makes errors invisible — defeats the point |
set -e; cmd1; cmd2; cmd3 for a “transaction” |
use a function that returns on failure, or trap ERR | only catches one command at a time |
Forgetting IFS=$'\n\t' |
add it to the header | many array/glob bugs vanish |
Next → set -o pipefail