Here's a script that will ruin your day:
#!/bin/bash
cd /nonexistent/folder
rm -rf *
echo "Done"
The cd fails because the folder doesn't exist. Bash ignores the failure. rm -rf * runs in whatever directory you were already in. The script prints "Done." You have no idea anything went wrong until you notice your files are gone.
This isn't a contrived example. This is the actual failure mode of every bash script that doesn't have error handling. Bash's default behavior is to keep running after errors. Every command after a failure executes as if everything is fine. It's the most dangerous default in the entire language.
Two lines fix it.
The Template
#!/bin/bash
set -euo pipefail
trap 'echo "Error on line $LINENO — script stopped." >&2' ERR
# Your script starts here
Add this to the top of every script you write. Every single one. No exceptions.
What Each Flag Does
-e (errexit) — Stop the script immediately if any command exits with a non-zero status. This is the one that prevents the cd + rm disaster above. If cd fails, the script stops right there. rm never runs.
-u (nounset) — Treat any undefined variable as an error. Without this, $UNDEFINED_VAR silently becomes an empty string. Imagine this:
BACKUP_DIR="" # Oops, forgot to set this
rm -rf "$BACKUP_DIR"/*
That expands to rm -rf /*. That's your entire filesystem. With -u, bash catches the empty variable and stops before the rm ever runs.
-o pipefail — Make a pipeline fail if ANY command in it fails, not just the last one. Without this:
cat nonexistent-file.txt | grep "pattern" | wc -l
cat fails, but grep gets empty input, and wc -l counts zero lines and exits successfully. The pipeline reports success even though the first command failed. With pipefail, the pipeline's exit status is the failure from cat, so -e catches it.
The trap Line
trap 'echo "Error on line $LINENO — script stopped." >&2' ERR
This tells bash: "When an error happens, before you exit, run this command." It prints which line number failed and writes to stderr (>&2). Without this, the script just stops silently and you have to guess which line caused it.
In a 200-line script, "it failed somewhere" is useless. "Error on line 47" is actionable.
The Real-World Difference
Without error handling:
#!/bin/bash
cd /tmp/build
make
make install
echo "Build complete"
If cd fails (the directory doesn't exist), make runs in the wrong directory. If make fails (compilation error), make install installs whatever was there from last time. The script prints "Build complete" regardless.
With error handling:
#!/bin/bash
set -euo pipefail
trap 'echo "Error on line $LINENO" >&2' ERR
cd /tmp/build
make
make install
echo "Build complete"
If cd fails, the script stops and prints "Error on line 5." If make fails, same thing. echo "Build complete" only runs if every single command before it succeeded.
The One Gotcha
set -e changes how you write conditional logic. This fails:
set -e
grep "pattern" file.txt # exits 1 if pattern not found
echo "This never runs"
grep returns exit code 1 when it finds no matches. With -e, that's treated as an error and the script stops. But you didn't want it to stop — you just wanted to check if the pattern exists.
The fix is to use it in a conditional:
set -e
if grep -q "pattern" file.txt; then
echo "Found it"
else
echo "Not found"
fi
When a command is part of an if condition, -e doesn't trigger on its exit code. This is the standard pattern.
Or suppress the exit code explicitly:
grep "pattern" file.txt || true
The || true means "if grep fails, run true instead" — and true always succeeds, so -e doesn't trigger.
Where to Go from Here
Every script on BashSnippets.xyz uses set -euo pipefail and the CHECK="✓" / CROSS="✗" pattern. If you want a head start, the Bash Boilerplate Generator builds a complete script template with error handling, logging, and cleanup traps already configured. Pick your options, copy the output, and start writing from a safe foundation.
Full breakdown with more examples, the trap pattern, and common pitfalls:
bashsnippets.xyz/snippets/bash-error-handling.html
If you only take one thing from this: add set -euo pipefail to the top of every script. It takes 3 seconds and prevents the kind of failures that take hours to recover from.