Linux/Bash

Bash Strict Mode Pitfalls With Set E and Pipefail in Production Scripts

4 min read by DebuggedIt

Quick answer

Bash strict mode, usually written as set -euo pipefail, makes scripts stop on errors, unset variables, and failed pipeline stages. It catches many mistakes,...

Bash strict mode, usually written as set -euo pipefail, makes scripts stop on errors, unset variables, and failed pipeline stages. It catches many mistakes, but set -e has surprising exceptions and is not a complete error handling strategy. In production scripts, combine strict mode with explicit checks, a trap for cleanup, and careful handling of commands that are allowed to fail.

What Do set -e, set -u, and pipefail Actually Do?

Each option addresses a different failure mode. Together they are often called unofficial strict mode, but they are separate features with separate quirks.

  • set -e: exit immediately if a command returns a non-zero status, with several important exceptions.
  • set -u: treat the use of an unset variable as an error instead of silently expanding to an empty string.
  • set -o pipefail: make a pipeline return the status of the last command that failed, not just the final command.

The unset variable check prevents a classic disaster. A script that runs rm -rf with a variable that turned out empty can delete the wrong directory. With set -u, that mistake aborts the script instead.

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

backup_dir="${BACKUP_DIR:?BACKUP_DIR is required}"
rm -rf -- "${backup_dir:?}/old"

Where Does set -e Surprise You?

The most important thing to know is that set -e is not applied everywhere. Bash deliberately ignores failures in certain contexts, because those commands are being used as tests.

A failing command does not trigger exit when it is part of an if condition, a while or until condition, part of a list joined with && or || (except the last command), or when its status is being negated with an exclamation mark. This means a failure inside a function called from an if statement can be silently ignored, even though the same function would abort the script when called directly.

Another surprise involves command substitution and local. In a line such as local value=$(some_command), the exit status you see is that of local, which succeeds, so a failure in the substitution is masked. Declare the variable first and assign on a separate line to catch the error.

Behavior also differs between shells and versions in some edge cases, so consult the Bash manual for details when a script depends on them.

How Does pipefail Change Pipelines?

Without pipefail, a pipeline succeeds if its last command succeeds, even if an earlier stage failed. A command like curl fetching a file and piping it into a parser would appear to work when the download failed but the parser accepted empty input.

With pipefail enabled, the pipeline fails if any stage fails. There is a catch: some commands exit non-zero in normal situations. For example, grep returns 1 when it finds no match, which is not an error in many scripts. Under strict mode, that would abort the script, so handle it explicitly.

count=$(grep -c "ERROR" app.log || true)

if ! grep -q "ready" status.txt; then
    echo "service not ready" >&2
    exit 1
fi

Also be aware of SIGPIPE. When a downstream command such as head closes early, the upstream command may be killed with a broken pipe, which pipefail then reports as a failure.

What Is a More Reliable Error Handling Approach?

Treat strict mode as a safety net, not the whole plan. Check critical commands explicitly and give clear error messages, so failures are easy to diagnose.

  • Use trap with ERR and EXIT to log failures and clean up temporary resources.
  • Check required commands and variables at the top of the script.
  • Use explicit if checks or || exit for operations where failure needs a specific response.
  • Quote variables and use -- before filenames to avoid word splitting and option injection.
  • Run ShellCheck in CI to catch common mistakes before they reach production.

Frequently Asked Questions

Should every Bash script use set -euo pipefail?

It is a good default for automation scripts, but understand its limits. For interactive or highly exploratory scripts, it can get in the way.

Why did my script continue after a failed command?

The command was probably in a context where set -e is ignored, such as an if condition, an AND-OR list, or a function called in those contexts.

How do I allow a command to fail?

Append || true, or test it in an if statement. Add a comment explaining why the failure is acceptable.

Does set -u break optional variables?

It errors on unset ones. Use default expansion such as ${VAR:-default} when a variable is allowed to be missing.

Summary

Strict mode improves safety but has real gaps. Combine set -euo pipefail with explicit checks, traps, quoting, and ShellCheck. Consult the Bash manual for exact behavior in your version.