Industrial-grade error handling in Bash
set -Eeuo pipefail honestly, ERR traps, error propagation and stack traces.
A production Bash script should fail loudly, at the first sign of trouble, with enough context to debug it — and never leave the system half-changed. That takes more than set -e. You want the full strict-mode preamble, an ERR trap that prints where it died, and a deliberate view of what errexit does and does not catch, so you are never surprised by a script that sailed past a failure.
The honest strict-mode preamble
set -Eeuo pipefail is the baseline: -e exits on an uncaught error, -u treats unset variables as errors, -o pipefail makes a pipeline fail if any stage fails, and -E makes the ERR trap survive into functions, subshells and command substitutions. Setting IFS to just newline and tab removes a whole class of word-splitting surprises. But be honest about the limits — errexit has famous blind spots, and pretending it is a safety net it is not will bite you.
#!/usr/bin/env bashset -Eeuo pipefailIFS=$'\n\t'# a clear failure with location, then run any cleanuptrap 'rc=$?; echo "ERR rc=$rc at ${BASH_SOURCE[0]}:${LINENO}: ${BASH_COMMAND}" >&2' ERRtrap 'cleanup' EXITcleanup() { [[ -n "${tmp:-}" ]] && rm -rf "$tmp"; }tmp="$(mktemp -d)"
Where errexit silently does nothing
errexit is suppressed whenever a command’s exit status is being inspected: anything on the left of && or ||, any command in an if or while condition, a function called in those positions, and (crucially) a bare command substitution used in an assignment. So local out=$(cmd) will not abort on cmd’s failure — the local builtin succeeds. Separate the declaration from the assignment, and prefer explicit checks for the paths that matter.
# BROKEN: script continues even though curl failed, because `local` succeedsfetch() { local body=$(curl -fsS "$1"); echo "$body"; }# FIX: declare first, assign second — now errexit sees the real statusfetch() {local bodybody=$(curl -fsS "$1") || { echo "fetch failed: $1" >&2; return 1; }printf '%s' "$body"}# check a pipeline stage explicitlyif ! grep -q ready <(status_cmd); then echo "not ready" >&2; exit 1; fi
ERR traps and pseudo stack traces
With -E set, an ERR trap fires on any untrapped failure anywhere. Combined with the BASH_SOURCE, BASH_LINENO and FUNCNAME arrays you can print a real call stack, which turns a one-line "it broke" into a map of exactly how execution got there. For a tool other people run, that stack trace is the difference between a self-service fix and a support ticket.
trace() {local rc=$? i=0echo "FATAL rc=$rc: ${BASH_COMMAND}" >&2while caller "$i" >/dev/null 2>&1; do# caller prints: <lineno> <function> <source>echo " at $(caller "$i")" >&2; i=$((i+1))doneexit "$rc"}trap trace ERR