It's 2 a.m. and your nightly backup script reports "success." Three weeks later you need the backup, and the folder is empty. The script never failed. It just kept going after cd quietly didn't work, then happily ran tar on the wrong directory.

Most of us have written a script like this. Bash is forgiving by default, and that is exactly the problem: when a command fails, Bash shrugs and runs the next line. Three small lines at the top of your script change that behavior, and they catch a surprising share of real-world mistakes.

Bash's default is "keep going no matter what." For automation, that's the wrong default.

The Safety Header: Three Lines Worth Memorizing

Here is the header I put at the top of nearly every script I write:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Each piece does a different job. set -e tells Bash to exit when a command returns a non-zero status. set -u treats the use of an undefined variable as an error. set -o pipefail makes a pipeline fail if any command in it fails, not just the last one. The IFS line stops word-splitting on spaces, which saves you when filenames contain them.

Think of it as a seatbelt. It won't prevent every crash, but the cost of wearing it is almost zero, and the first time it saves you, you'll never skip it again.

Why set -e Alone Isn't Enough

Beginners often add set -e and assume they're protected. They're only partly right. Consider this:

#!/usr/bin/env bash
set -e

grep "ERROR" /var/log/app.log | sort | uniq -c > summary.txt
echo "Report created"

If /var/log/app.log doesn't exist, grep fails, but sort and uniq succeed, so the pipeline's exit status is zero. The script prints "Report created" next to an empty file. Adding pipefail fixes this: the pipeline now returns the failing status, and set -e stops the script.

The takeaway is that these options work as a team. -e reacts to failures, pipefail makes sure failures are visible, and -u turns typos into loud errors instead of silent blanks.

AD

The Typo That Deletes the Wrong Folder

The classic horror story involves an unset variable:

rm -rf "$BUILD_DIR/"*

If BUILD_DIR is empty because of a typo or a missing environment variable, this expands to rm -rf /*. With set -u, Bash stops immediately with an "unbound variable" error before anything is deleted.

When a variable is allowed to be empty, say so explicitly with a default:

LOG_LEVEL="${LOG_LEVEL:-info}"       # fall back to "info"
: "${DB_HOST:?DB_HOST must be set}"  # abort with a clear message

The :? form is underrated. It documents which settings are required and produces a readable error message, which beats a cryptic failure thirty lines later.

Cleaning Up After Yourself With trap

Failing fast is good, but a script that dies halfway often leaves temporary files, lock files, or half-written output behind. Bash offers trap to run cleanup code whenever the script exits, successfully or not:

tmpdir="$(mktemp -d)"
cleanup() {
  rm -rf "$tmpdir"
}
trap cleanup EXIT

curl -fsS https://example.com/data.json -o "$tmpdir/data.json"
python3 process.py "$tmpdir/data.json"

The EXIT trap fires on normal completion, on errors caught by set -e, and on most interrupts. Note the -f flag on curl: without it, an HTTP 404 page would be downloaded as if it were valid data, and set -e would never notice.

AD

When You Expect a Command to Fail

Strict mode doesn't mean every non-zero exit is a disaster. grep -q returns 1 when it finds nothing, which is often a perfectly fine answer. Handle those cases on purpose:

if grep -q "maintenance" /etc/motd; then
  echo "Maintenance banner is set"
fi

# Or explicitly tolerate a failure:
count="$(grep -c "ERROR" app.log || true)"

Commands inside an if condition are exempt from set -e, so the first form is safe. The || true pattern in the second form should be used sparingly and only where you've decided that failure is acceptable. If you find yourself sprinkling it everywhere, the script probably needs restructuring.

OptionWhat it catchesTypical symptom without it
set -eFailed commandsScript continues after an error
set -uUndefined variablesEmpty string silently used in a path
set -o pipefailFailures mid-pipelineEmpty output reported as success
trap ... EXITLeftover stateTemp files and stale locks

Add a Linter and Stop Guessing

Even with a safety header, Bash has traps for the unwary: unquoted variables, [ ] vs [[ ]] differences, and loops over ls output. A free tool called ShellCheck reads your script and points out these problems, usually with a link explaining why. On most Linux distributions it's one package-manager command away:

sudo apt install shellcheck
shellcheck backup.sh

Run it before you commit. It's like having a patient reviewer who has seen every Bash mistake before and never gets tired of pointing them out.

Summary

Strict mode isn't magic, but it moves your scripts from "fails silently" to "fails loudly," and loud failures are the ones you can fix. Start every script with set -euo pipefail, quote your variables, use trap for cleanup, and let ShellCheck review your work. It costs five minutes today and can save you from a very long night later.

Your future self, staring at a pager at 2 a.m., will quietly thank you.