The Backup That Wasn't: Why `false | true` Should Fail Your Script
A default Unix pipeline hides earlier failures. Learn how `set -euo pipefail` catches truncated backups and typo'd variables, and where strict mode still falls short.
09 Mar 2026, 06:09 UTC

Your nightly backup script exits with status 0. The log says "done." But the archive sitting in /var/backups is a truncated gzip file, and the database dump inside it is missing half its tables. Nothing alerted. Nothing failed. The reason is baked into how Unix pipelines report status: by default, only the last command's exit code matters.
Adopting set -euo pipefail as a baseline in bash scripts is one of the cheapest ways to convert silent corruption into loud, immediate failure. It is not a universal safety net, and it is not portable to every /bin/sh, but for scripts that move, transform, or back up data, it catches three distinct classes of error before they reach disk.
The Pipeline Lie
In a POSIX shell, a pipeline's exit status is the exit status of the last command. That means false | true succeeds. If the first stage fails but the second stage succeeds, the failure vanishes.
This matters most when the first stage produces data and the second stage packages it. Consider a database dump piped to a compressor. If the dump process dies halfway through—network interruption, lock timeout, corrupted table—the compressor still sees a valid input stream, writes a truncated archive, and exits 0. The script reports success.
pipefail changes the rule: if any stage in the pipeline fails, the pipeline fails. It is supported in bash, zsh, and ksh93, but not in strict POSIX shells like dash.
The Empty Variable Trap
The second failure mode is quieter. A typo in a variable name—$BACKUP_DIR versus $BACKUPDIR—does not produce an error in a default shell. The variable expands to an empty string, and the command runs with a missing path. Depending on the command, that can mean writing to the current working directory, deleting nothing, or overwriting an unintended target.
set -u treats expansion of an unset variable as a fatal error. It turns a silent typo into an immediate stop.
The Abort-on-Error Myth
set -e tells the shell to exit when a simple command fails. It is the most familiar of the three options and the most misunderstood. It does not apply inside if, while, or until conditionals, nor in && or || lists, nor when a pipeline is negated with !. A command that returns non-zero as part of a conditional test is expected behavior, not an error.
There is also a version wrinkle: in bash versions before 4.4, set -e could leak into function calls and command substitutions in ways that differ from later releases. If you maintain scripts across heterogeneous systems, test on your oldest target shell.
A Worked Example: Database Backup
Here is a minimal backup script that moves a database dump through a compressor. Run it from a maintenance directory or cron context where the target directory is writable.
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups}"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
DUMP_FILE="${BACKUP_DIR}/db_${TIMESTAMP}.sql.gz"
# Requires a MySQL/MariaDB client with dump privileges.
mysqldump --single-transaction mydatabase | gzip > "${DUMP_FILE}"
Without pipefail, a failed mysqldump still produces a file and exits 0. With strict mode, the pipeline fails, the script aborts, and the truncated file is not left behind as a false success.
The ${BACKUP_DIR:-/var/backups} syntax is deliberate. It supplies a default when the variable is unset, which satisfies set -u while still allowing an environment override.
Where Strict Mode Breaks Down
Strict mode is not free. It breaks scripts that intentionally test for failure. If your script runs grep -q pattern file || echo "not found", the || list already suppresses set -e; that is fine. But if you rely on a command returning non-zero inside a bare if block and then continue, set -e will not abort there, which can mask a real error if you expected it to.
set -u will also break scripts that read optional environment variables. Any variable that might legitimately be unset needs a default via ${VAR:-default} or an explicit +u guard around the expansion.
Finally, pipefail is not POSIX. If you write #!/bin/sh and the system's sh is dash, the script will fail with a syntax error. Declare your interpreter explicitly as #!/usr/bin/env bash or #!/bin/bash when you use these options.
How to Verify Before You Commit
Before rolling strict mode into a production script, confirm the behavior in your target shell. Run these checks in a terminal or CI job on the same OS image that will execute the script.
Check pipefail:
bash -c 'set -o pipefail; false | true; echo exit=$?'. Expected result: a non-zero exit status. Compare withsh -c 'false | true; echo $?', which prints 0 on a POSIX shell.Check unset variables:
bash -c 'set -u; echo ${UNSET_VAR}'. Expected result: an error such asUNSET_VAR: unbound variable, not an empty line.Syntax check:
bash -n script.sh. This parses without executing.Dry run: Execute the script in a disposable directory with dummy paths before pointing it at real data.
If you must support multiple shells, run the script under dash, bash, and zsh and observe which options are accepted and where exit codes diverge.
The Takeaway
set -euo pipefail is a baseline, not a guarantee. It converts the most common silent failures—truncated pipelines, typo'd variables, and ignored command errors—into visible stops. Adopt it in scripts that mutate or move data, guard the intentional exceptions with || true or default expansions, and verify the behavior on your oldest target shell before you trust it with a backup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.