Skip to content
Go back

Bash Strict Mode: set -euo pipefail Explained

· Updated:
By SumGuy 6 min read
Bash Strict Mode: set -euo pipefail Explained

The Problem: Silent Failures

Write a bash script without strict mode. It does this:

#!/bin/bash
cd /nonexistent/path
rm -rf *

Script runs. Doesn’t cd because the path doesn’t exist. But it continues anyway. Then it deletes everything in the current directory. Your morning is ruined.

Bash defaults to ignoring errors. That’s insane for scripts. set -euo pipefail fixes it.

What Each Flag Does

set -e (errexit)

Exit immediately if any command exits with a non-zero status.

#!/bin/bash
set -e
cd /nonexistent
echo "This never runs"

The cd fails. Script exits. echo never runs.

Without set -e, the script continues and echo runs in the wrong directory.

Gotcha: Commands in an if-statement don’t trigger -e:

Terminal window
set -e
if cd /nonexistent; then
echo "OK"
else
echo "Failed"
fi
echo "Script continues"

This runs to completion because cd is in a conditional. The script wants you to handle it explicitly.

set -u (nounset)

Treat undefined variables as errors.

#!/bin/bash
set -u
echo $UNDEFINED_VAR

Output: bash: UNDEFINED_VAR: unbound variable and the script exits.

Without set -u, it echoes an empty line. Silent failure. You spend an hour debugging why your script doesn’t work.

Gotcha: Parameters like $@ and $* are special. If you want to safely handle missing parameters:

#!/bin/bash
set -u
filename="${1:-default.txt}"

The ${1:-default.txt} syntax means: use $1 if set, otherwise use “default.txt”. No error.

set -o pipefail

If any command in a pipe fails, the pipeline’s exit status reflects that failure instead of hiding it behind the last command.

#!/bin/bash
set -e
set -o pipefail
cat nonexistent.txt | grep "ERROR" | wc -l

Without pipefail, cat fails but the pipeline reports the status of wc (success), so set -e never sees a problem. The error is hidden.

With pipefail, the pipeline reports cat’s failure, and set -e catches it and exits. You see the problem. pipefail on its own doesn’t stop the script: it only changes what the pipeline’s exit status is. Pair it with -e (or check the status yourself) to act on it.

Real example:

Terminal window
curl https://api.example.com/data | jq '.results'

If curl fails (network error), the script should fail. Without pipefail, jq succeeds on empty input and the script continues. Hours of debugging later, you realize there was no data.

Putting It Together

#!/bin/bash
set -euo pipefail
# Declare variables before use
readonly LOG_FILE="${1:-/var/log/app.log}"
readonly PROCESSED_DIR="/tmp/processed"
# Early exit if dependencies are missing
command -v jq >/dev/null || { echo "jq is required"; exit 1; }
mkdir -p "$PROCESSED_DIR"
cd "$PROCESSED_DIR" || exit 1
# This pipe will exit if any command fails
cat "$LOG_FILE" | grep "ERROR" | wc -l

Common Gotchas and Fixes

Testing with test or [[ ]]

These return non-zero on false. With -e, they cause early exit, but only when they’re not shielded by a conditional or a &&/|| chain:

Terminal window
set -e
if [ -f "$file" ]; then
echo "File exists"
fi

This is fine, [ is in a conditional.

This is also fine, even though it looks risky:

Terminal window
set -e
[ -f "$file" ] && echo "File exists"

Bash exempts every command in a &&/|| list except the last one. [ -f "$file" ] isn’t the last command here, so the file missing doesn’t trigger -e. The script keeps going.

The gotcha runs the other way. If the last command in the chain fails, -e does trigger:

Terminal window
set -e
mkdir -p "$dir" && cd "$dir"

cd is the last command in that && list, so if it fails, the script exits.

Returning from Functions

Functions respect -e. If a command in a function fails, the function exits and the script exits:

Terminal window
set -e
process_file() {
cat "$1" | grep "ERROR"
echo "Processed" # Runs only if grep succeeds
}
process_file /var/log/app.log

If the file doesn’t exist or has no errors, grep fails, the function exits, and the script exits. The echo never runs.

Disabling -e Temporarily

Sometimes you want to ignore an error. Use set +e:

Terminal window
set -e
set +e
rm /tmp/maybe_doesnt_exist.txt
set -e
echo "Continued"

Or run a command with || true:

Terminal window
set -e
rm /tmp/maybe_doesnt_exist.txt || true
echo "Continued"

|| true means: if rm fails, run true (which always succeeds). The script continues.

The IFS Gotcha With -u

One subtle issue with -u: the $@ and $* special variables expand to nothing when no arguments are passed. With -u, you’ll get “unbound variable” errors if you reference them carelessly.

Safe pattern for optional args:

#!/bin/bash
set -euo pipefail
# This will fail with -u if no args passed:
# first_arg="$1"
# Safe: use default value syntax
first_arg="${1:-}" # Empty string if not provided
named_arg="${2:-default}" # "default" if not provided
if [ -n "$first_arg" ]; then
echo "Got arg: $first_arg"
fi

The ${VAR:-default} syntax is your friend under set -u. It provides a default value instead of triggering the unbound variable error.

Add an ERR Trap

Pair strict mode with a trap to print where the script died:

#!/bin/bash
set -euo pipefail
trap 'echo "Error on line $LINENO"' ERR
echo "Step 1"
false # This triggers the trap
echo "Step 2" # Never runs

Output:

Step 1
Error on line 7

For debugging, print more context:

Terminal window
trap 'echo "Error on line $LINENO: $BASH_COMMAND"' ERR

Now you see the exact command that failed. Beats staring at a silent non-zero exit code.

Bottom Line

Every bash script over 20 lines should start with:

#!/bin/bash
set -euo pipefail

It makes scripts fail fast instead of corrupting data at 2 AM. Your ops team will thank you.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Ventoy: Boot Any OS, Any Time
Next Post
The sudoers Mistake Everyone Makes Once

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts