# Strict Mode: set -euo pipefail and Its Caveats — Bash / Shell Scripting

Source: https://www.skillbyai.com/en/bash/r-strict

> Make scripts fail fast and understand where errexit does not help.

## Fail early instead of carrying on broken

By default Bash keeps running after a command fails, uses empty strings for unset variables and ignores failures inside pipelines, so a script can continue after a failed `cd` and delete files in the wrong directory. Many scripts start with **`set -euo pipefail`**: **`-e`** (errexit) exits when a command fails; **`-u`** (nounset) treats use of an unset variable as an error, catching typos; **`-o pipefail`** makes pipelines fail if any part fails. These help a lot, but `-e` has well-known **caveats**: it is ignored for commands in `if` conditions, in `while`/`until` conditions, on the left of `&&` or `||`, and in functions called from those contexts; a command substitution's failure inside `local var=$(cmd)` is masked by `local`'s own success; and in arithmetic, `(( count++ ))` returns status 1 when the old value is 0, which can exit your script unexpectedly. So treat strict mode as a safety net, not a substitute for **explicit checks** of commands whose failure you need to handle.

## Fail fast instead of cascading

Strict mode stops the script at the first unexpected failure instead of letting it carry on in a broken state.

![A row of step boxes where the third is red; on one path the steps after it continue in red, on the other path a stop sign appears right after it.](assets/figures/bash/section-6-map.svg) — Figure 6.1 — With and without strict mode.

## Strict mode and its traps

Each comment marks a case where -e does not behave as people expect.

```bash
#!/usr/bin/env bash
set -euo pipefail

cd /srv/app                     # if this fails, the script exits (good)

local_example() {
    local version
    version=$(git describe --tags)   # separate declaration keeps the failure visible
    echo "$version"
}

if grep -q "ready" status.txt; then   # failure here is a condition, not an exit
    echo "ready"
fi

count=0
(( count++ )) || true           # (( 0 )) is "false": without || true, -e exits here
count=$(( count + 1 ))          # assignment form never returns a failing status

rm -f "${TMPDIR:?}/build-cache" # :? guards against an empty variable deleting the wrong path
```

## Guard rm with :?

`rm -rf "$dir/"` with an empty `$dir` becomes `rm -rf /`. `rm -rf "${dir:?}/"` stops the script with an error if `dir` is unset or empty. Use it for every destructive command built from variables.

**Quiz:** Why can `local out=$(failing_command)` hide a failure even with set -e?

- [ ] set -e is disabled inside functions
- [ ] Command substitution always returns 0
- [x] The status of the line is that of the local builtin, which succeeds
- [ ] local makes variables read-only

*Answer:* The status of the line is that of the local builtin, which succeeds. local returns its own success status, masking the command substitution's failure; declare and assign separately.
