skip to content

In a bash script, what does `trap 'handler' ERR` give you that `set -e` alone does not, and why is it usually paired with `set -E`?

level: seniorimportance: nice to knowfreq 32%

answer

  1. errexit aborts, this one explains
  2. who failed, where, with what status
  3. not inherited by functions by default
  4. errtrace turns inheritance on
  5. same blind spots as set -e

basics

~20 s

An ERR trap runs your handler on the same failures that make set -e abort, so the script can report the failing line, command and status instead of dying silently. set -E (errtrace) is needed because otherwise the ERR trap is not inherited by shell functions, command substitutions or subshells.

solid answer

~50 s

`set -e` aborts, but says nothing: the operator gets an exit code and no clue which line produced it. `trap 'handler' ERR` fires on the same events, letting you print a diagnostic — typically `$LINENO`, `$BASH_COMMAND` for the command text, `$?` for its status, and `${BASH_SOURCE[0]}` or `${FUNCNAME[@]}` for location — before the shell exits. By default the ERR trap is *not* inherited by shell functions, command substitutions or subshells, so a failure inside a function would produce nothing at all; `set -E` (`errtrace`) turns that inheritance on, which is why the strict-mode line is often written `set -Eeuo pipefail`. Note that ERR shares errexit's blind spots exactly — it does not fire for a command in an `if` condition, a non-final `&&`/`||` element, or a negated command. Both `set -E` and the ERR trap are bash extensions, absent from POSIX sh.

code

bash · 10 lines
bash
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'echo "$0:$LINENO: \"$BASH_COMMAND\" exited $?" >&2' ERR

work() {
  false
}

work
echo "never reached"

go deeper

for a junior

Know that set -e aborts a script silently, and that bash can be told to run a handler on that failure so the log says which line broke. Recognising trap ... ERR when you read a script is enough at this level.

for a middle

Explain what the handler can report — $BASH_COMMAND, $LINENO, $?, ${BASH_SOURCE[0]} — and that $? must be captured first. Be able to state that the trap reports and does not itself decide whether the script stops.

for a senior

Show the set -E inheritance gap and why a handler installed at the top of a script prints nothing when the failure happens inside a function. Note that ERR inherits errexit's blind spots exactly, so it is a diagnostic layer rather than an error-handling strategy.

for a principal

Decide where this belongs. Weigh a standard error-reporting preamble across a fleet of scripts against structured logging or moving the work into a real program, and be explicit that neither ERR nor errexit is available when the target interpreter is a POSIX sh.

## The problem ERR solves Under `set -e`, a failure produces exactly one artifact: a non-zero exit status. Whatever the failing command printed may or may not be useful, and the script itself contributes nothing — no line number, no indication of which of the forty commands in the file gave up. For a script running in a cron job or a container entrypoint, where the only evidence is a log, that is a genuine operational problem. The `ERR` pseudo-signal exists for this. `trap COMMANDS ERR` arranges for `COMMANDS` to run whenever a command would cause `set -e` to exit. It fires on the same events as errexit, whether or not errexit is actually enabled — the two are independent switches over the same event. ## The minimal useful handler ```bash #!/usr/bin/env bash set -Eeuo pipefail trap 'echo "line $LINENO: \"$BASH_COMMAND\" exited $?" >&2' ERR rsync -a src/ dst/ ``` The trap argument is single-quoted so the variables expand when the trap runs, not when it is installed. The pieces: - `$LINENO` — the line number of the failing command. - `$BASH_COMMAND` — the text of the command currently executing, which inside an ERR trap is the command that just failed. - `$?` — its exit status. If the handler is a function, capture this on its very first line (`local status=$?`), because any command in the handler overwrites it. - `${BASH_SOURCE[0]}` — the file, which matters once the script sources libraries. - `${FUNCNAME[@]}` and `${BASH_LINENO[@]}` — the call stack, for a handler that prints a traceback. ## Why `set -E` is not optional By default, the `ERR` trap is **not inherited** by shell functions, by command substitutions, or by commands executed in a subshell. That default is close to useless in a real script, because almost all the work happens inside functions: install a handler at the top, put a failing command inside `deploy()`, and nothing prints. `set -E` (long form `set -o errtrace`) switches that inheritance on. It is the direct analogue of `set -T` (`functrace`), which does the same for `DEBUG` and `RETURN` traps. This is why the strict-mode incantation is often written with four letters, `set -Eeuo pipefail`. ```bash set -e trap 'echo "failed: $BASH_COMMAND" >&2' ERR work() { false; } work # without -E: nothing printed, script just exits ``` Add `set -E` and the same script prints `failed: false` before exiting. ## The blind spots are exactly errexit's The ERR trap is not executed when the failing command is: - part of the condition of `if`, `while` or `until`; - part of a `&&` or `||` list, except the final command; - any command in a pipeline other than the last; - negated with `!`. That symmetry is the point: ERR reports the failures errexit would act on, and is equally silent about the ones errexit ignores. An ERR trap therefore does **not** rescue you from the function-called-from-an-`if` problem — it inherits it. If you want an unconditional cleanup that runs on every exit path, that is a different trap with different semantics. ## Does the trap stop the script? No. `ERR` reports; it does not decide. With `set -e` also enabled, the shell exits after the handler returns, using the failed command's status. With errexit off, the handler runs and execution continues on the next line, which is occasionally what you want for a script that must attempt every step and summarise at the end. If the handler itself should terminate the script, it ends with an explicit `exit`. ## When to reach for it An ERR trap is a diagnostic aid, not an error-handling strategy. It shines where a script is long, mostly linear, and run unattended: you get a one-line "failed at deploy.sh:112 running `helm upgrade …` (exit 1)" for free, on every failure, without writing a message at each call site. It does not replace explicit `|| die` checks for the failures you actually anticipate, and it does nothing about the cases errexit already misses. Finally, keep portability in mind: `trap ... ERR` and `set -E` are bash (and ksh) extensions. A script that must run under a POSIX `/bin/sh` such as dash cannot rely on either, and has to check errors explicitly at each call site instead.

  • Inside an ERR handler written as a function, why must `$?` be read on the first line?
    `$?` always holds the status of the most recently completed command. The moment the handler runs anything — even an `echo` or a `[[ ]]` test — that value is replaced. Capturing it first, with something like `local status=$?`, preserves the failing command's status so the handler can report it and, if desired, `exit` with it.
  • Does an ERR trap stop the script by itself?
    No. It only runs the handler. Whether the script then terminates is decided by `set -e`: with errexit on, the shell exits after the handler returns, using the failed command's status; with errexit off, execution continues at the next line. A handler that must terminate the script has to call `exit` explicitly.
  • You want a stack trace rather than a single line. Which bash arrays give you that?
    `${FUNCNAME[@]}` holds the function call chain with the current function at index 0, `${BASH_SOURCE[@]}` the corresponding source files, and `${BASH_LINENO[@]}` the line numbers of each call site. Walking the three in parallel from index 0 upward prints a caller-by-caller trace; the `caller` builtin exposes the same information one frame at a time.

saying these in an interview costs you the question

  • Thinks the ERR trap fires for every non-zero status anywhere
  • Assumes the handler runs inside functions without set -E
  • Reads $? after already running a command in the handler
  • Believes the ERR trap replaces explicit error checks
  • Expects ERR to run on a normal successful exit

context