Skip to content

Build1 publisher3 min readPublished

Adding -E made a silent ERR trap name the failing mkdir on line 5

Under set -euo pipefail, a failing mkdir inside a helper function killed a nine-line script while the ERR trap printed nothing. Adding one letter, -E, made the same trap report the command and the line it died on.

The Engineer · Build desk

Illustration accompanying Adding -E made a silent ERR trap name the failing mkdir on line 5

What happened

  • A nine-line test script set strict mode on line 2 and registered an ERR trap on line 3 to echo the failing command and its line number, then called a helper that ran mkdir on a path that cannot be created.
  • Run on bash 5.3.9, the script printed its start banner, then mkdir's own stderr complaint, then exited 1, and the registered trap printed nothing at all.
  • Changing the flag line to set -Eeuo pipefail produced the same mkdir error plus a trap line naming the command and location, mkdir /proc/export on line 5, and the same exit 1.
  • On the same machine, a separate function running local out=$(false) under set -euo pipefail continued to its next line and the script exited 0.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure A reviewer reading the helper cannot tell whether its failures are fatal, because the caller decides: one if wrapper disarms errexit for the whole function body.
  • cost Getting captured-output failures back costs a second line at every site that assigns a command's output into a local, paid by whoever maintains the script.
  • capability With errtrace on and a handler that grabs $? before anything else, a nightly job can name the command, line and exit status without per-command error checks.
  • constraint Anything paging off the ERR handler needs deduplication, since one failure in an explicit subshell will deliver two identical reports.

Errexit's own exemptions are why this bites inside functions and not at the top level. Bash counts a command as checked when it is the condition of an `if`, when it sits left of `&&` or `||`, or under `!`. The exemption also covers everything inside a function called from one of those positions [10]. Wrap a careful helper in `if helper; then` and every line inside it loses errexit, and the output will not tell you [11].

An ERR trap looks like the place to recover the diagnostic, because it fires on the same conditions that make `-e` exit [8]. By default it is not inherited by functions, command substitutions or subshells [6]. In the nine-line test [23], the `mkdir` sat one level down inside the function. Errexit killed the script. The trap, registered in the top-level shell, missed the failure inside the function's scope [9]. `-E`, errtrace, is the option that makes the trap follow into functions and subshells [7].

The handler the author now runs takes the exit status first: `local code=$?` on the first line, before anything else overwrites it. Then one echo carries `BASH_COMMAND`, `BASH_LINENO[0]` and that code to stderr [14]. The dev.to post calls `-E` the flag missing from nearly every strict-mode line on the internet [22], and the author wrote: "If you take one thing from this, take the E." [15]

The other two letters each prevent a specific failure. `-u` turns an unset variable into an error instead of an empty string. When one place spells it `BUILDDIR`, that is the difference between `rm -rf "$BUILD_DIR/"` and `rm -rf /` [12]. `pipefail` is what stops `curl ... | jq ...` from reporting success when curl dies and jq parses the empty result [13].

The second silent spot is `local` itself. `local` is a command in its own right. The status bash sees is `local` succeeding at declaring the variable, so the command substitution's failure is discarded before anything looks at it [17]. Declare on one line, assign on the next, and the failure is visible again [18]. This is the bug behind ShellCheck's masked-return-value warning [19].

Both results come from one machine and one build [3]. For the `-E` change to alter your logs, your failures have to happen inside a function, a command substitution or a subshell [6]. The author says that is where most real failures happen [25]. A script whose commands all run at top level already gets the command and line from its trap, with or without the letter [6].

One behaviour to expect before you wire alerts to the handler: with `-E` set, a failure inside an explicit `( subshell )` fires the trap twice, once in the subshell and once in the parent as the non-zero status propagates. The author calls that expected and suggests handling it with a guard flag or by moving the work out of the subshell [20]. The EXIT trap is the separate piece. It runs on every termination, and that is what stops a script leaving a half-written file for the next stage to load as if it were complete [21].

What to watch

  • A second run on an older bash, such as the 4.x builds on many CI images, would show whether errtrace and the local masking behave identically there.
  • A ShellCheck or linter rule for a top-level ERR trap without -E would catch this in review instead of in a nightly log.
  • Teams paging on the ERR handler will learn from the first subshell failure whether the double fire produces two alerts.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories