Bash Strict Mode: set -euo pipefail Explained

By 

•

Updated on

•

11 min read

Enabling Bash strict mode with set -euo pipefail at the top of a shell script

By default, a Bash script will keep running after a command fails, treat unset variables as empty strings, and hide errors inside pipelines. That is convenient for one-off commands in an interactive shell, but inside a script it is a recipe for silent corruption: a failed backup step that still reports success, an unset path that expands to rm -rf /, a download pipeline that hides a failed curl.

Bash strict mode is a short set of flags you put at the top of a script to make the shell fail loudly instead. This guide explains each flag in set -euo pipefail, shows what it changes with real examples, and covers the cases where you need to turn it off. For a closer look at the set builtin and each option on its own, see our bash set command guide.

The Strict Mode Line

You will see this line at the top of many modern shell scripts:

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

Each flag addresses a different class of silent failure:

  • set -e exits immediately when a command returns a non-zero status.
  • set -u treats references to unset variables as an error.
  • set -o pipefail makes a pipeline fail if any command in it fails, not just the last.
  • IFS=$'\n\t' narrows word splitting so unquoted expansions do not split on spaces.

You can enable the options separately, but in practice they are almost always used together.

set -e: Exit on Error

Without set -e, a failing command in a script does not stop execution. The script happily continues to the next line:

no-strict.shsh
#!/usr/bin/env bash
cp /does/not/exist /tmp/dest
echo "Backup complete"

Running it prints both the error and the success message:

Terminal
./no-strict.sh
output
cp: cannot stat '/does/not/exist': No such file or directory
Backup complete

The script exits with status 0, so any caller thinks the backup worked. Adding set -e changes the behavior:

strict.shsh
#!/usr/bin/env bash
set -e
cp /does/not/exist /tmp/dest
echo "Backup complete"
Terminal
./strict.sh
echo $?
output
cp: cannot stat '/does/not/exist': No such file or directory
1

The script stops at the failing cp, never prints “Backup complete”, and exits with a non-zero status. Anyone scheduling this through cron or a CI pipeline now gets a real failure signal.

Opting Out for a Single Command

Sometimes you expect a command to fail and want to handle the result yourself. Append || true to tell set -e to ignore the exit status of that one command:

sh
grep "pattern" config.txt || true

You can also use if or &&, which set -e treats as deliberate checks:

sh
if ! grep -q "pattern" config.txt; then
  echo "pattern missing"
fi

This is the canonical way to check for something without exiting the script when it is not there.

set -u: Fail on Unset Variables

A classic shell footgun is a typo in a variable name. Without strict mode, Bash silently expands the misspelled variable to an empty string:

unset.shsh
#!/usr/bin/env bash
TARGET_DIR="/var/backups"
rm -rf "$TARGE_DIR/old"

The script deletes /old because $TARGE_DIR is empty. With set -u, the same script exits before the rm runs:

unset-strict.shsh
#!/usr/bin/env bash
set -u
TARGET_DIR="/var/backups"
rm -rf "$TARGE_DIR/old"
output
./unset-strict.sh: line 4: TARGE_DIR: unbound variable

Handling Optional Variables

Environment variables that may or may not be set need a default to play nicely with set -u. The ${VAR:-default} syntax provides one without touching the original:

sh
PORT="${PORT:-8080}"
echo "Listening on $PORT"

If $PORT is unset or empty, the script uses 8080. If it is set, the original value is kept.

set -o pipefail: Catch Failures Inside Pipelines

By default, the exit status of a pipeline is the exit status of the last command. Everything to the left can fail silently:

sh
curl -fsS https://bad.example.com/data | tee data.txt

If curl cannot reach the host, the pipeline still exits 0 because tee wrote its (empty) input successfully. The -f option makes curl return an error on HTTP failures such as a 404. Without it, curl exits 0 even when the server returns an error page.

With set -o pipefail, the pipeline returns the exit status of the rightmost command that failed, or 0 if every command succeeded:

pipefail.shsh
#!/usr/bin/env bash
set -o pipefail
curl -fsS https://bad.example.com/data | tee data.txt
echo "Exit: $?"
output
curl: (6) Could not resolve host: bad.example.com
Exit: 6

The exit status 6 comes from curl, not from tee. When more than one command fails, the last failure wins. For example, (exit 2) | (exit 3) | true returns 3.

This is the one flag that fixes the most “my script said it succeeded but clearly did not” bugs, and it is the one most often forgotten when people add just set -e.

Checking Each Command with PIPESTATUS

pipefail gives you a single exit status for the whole pipeline. To see the status of every command in it, use the PIPESTATUS array. Under set -e, handle a failed pipeline with || so the script does not exit before you can read its statuses:

sh
set -euo pipefail
false | true || echo "${PIPESTATUS[@]}"
output
1 0

The first value is the exit status of false, and the second is the exit status of true. Bash overwrites PIPESTATUS after every command, so expand it as the first command in the error handler, or save it to another array before running anything else. To learn more about exit codes, see our guide on Bash exit .

Handling grep with No Matches

grep exits with status 1 when it does not find a match. Under set -eo pipefail, that turns a pipeline that finds nothing into a failure, and the script stops without printing an error.

Here is a script that counts errors in a log file:

count-errors.shsh
#!/usr/bin/env bash
set -euo pipefail
matches=$(grep ERROR app.log | wc -l)
echo "errors: $matches"

If app.log has no ERROR lines, the script exits with status 1 and never prints the count. The same happens with grep -c, which also returns 1 when the count is zero.

To allow an empty result without hiding other errors, accept only grep status 1:

sh
matches=$( { grep ERROR app.log || [ "$?" -eq 1 ]; } | wc -l)

Now the script prints errors: 0 and continues when there are no matches. The test succeeds only for status 1. Other failures, such as a missing or unreadable app.log, leave the group unsuccessful, so pipefail and set -e still stop the script.

IFS: Safer Word Splitting

The Internal Field Separator (IFS) controls how Bash splits unquoted expansions into words. The default is space, tab, and newline, which means a filename with a space in it gets split into two arguments:

sh
files="one two.txt three.txt"
for f in $files; do
  echo "$f"
done
output
one
two.txt
three.txt

Setting IFS=$'\n\t' removes the space from the separator list, but still splits on tabs and newlines and does not prevent pathname expansion. Do not parse find or ls output this way, since valid filenames can contain tabs and newlines. Use globs, quoted arrays, or find -print0 with a NUL-delimited reader for filenames instead.

Even with a narrower IFS, always quote your expansions ("$var", "${array[@]}"). IFS tightening is a safety net, not a replacement for quoting.

When Strict Mode Gets in the Way

Strict mode is opinionated, and a few situations push back. The most common one is reading a file line by line with a while loop and a counter:

sh
count=0
while read -r line; do
  count=$((count + 1))
done < input.txt

That works fine, but if you increment with ((count++)) inside set -e, the script exits on the first iteration because ((count++)) returns the pre-increment value (0), which Bash treats as a failure. Use count=$((count + 1)) or ((count++)) || true to stay compatible.

Another common case is probing for a command:

sh
if ! command -v jq >/dev/null; then
  echo "jq is not installed"
  exit 1
fi

Using command -v inside an if is safe even under set -e, because set -e does not trigger on commands in conditional contexts.

If you need to temporarily disable a flag for a specific block, turn it off and back on:

sh
set +e
some_flaky_command
result=$?
set -e

This is cleaner than sprinkling || true everywhere when you have a block of commands that need softer error handling.

Common Variations

You will also see a few variations of the strict mode line in scripts and CI configuration files.

set -euxo pipefail

Adding -x turns on tracing. Bash prints every command to standard error before it runs, with variables already expanded:

trace.shsh
#!/usr/bin/env bash
set -euxo pipefail
name="world"
echo "Hello, $name"
output
+ name=world
+ echo 'Hello, world'
Hello, world

Each line that starts with + is a traced command. This variant is common in CI jobs, where the trace shows exactly which command failed. For more on tracing, see our bash set command guide.

set -uo pipefail

Some scripts leave out -e and check exit codes on their own. With set -uo pipefail, a failed pipeline still returns a non-zero status, but the script keeps running:

no-errexit.shsh
#!/usr/bin/env bash
set -uo pipefail
grep ERROR app.log | wc -l
echo "status: $?"
echo "still running"
output
0
status: 1
still running

The pipeline reports status 1 from grep, and the script continues to the next line. Use this form when you want to handle each failure yourself with if or $?.

set -eou pipefail

The order of the single-letter flags does not matter. set -eou pipefail and set -euo pipefail turn on the same three options, because -o takes the next word as its option name. The -euo order is the one you will see most often.

A Minimal Strict Script Template

Use this as a starting point for new scripts:

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

# Script body goes here.

It is three lines, and it catches the majority of silent failures that cause shell scripts to misbehave in production.

Quick Reference

FlagWhat it does
set -eExit when any command returns a non-zero status
set -uTreat unset variables as an error
set -o pipefailA pipeline fails if any command in it fails
IFS=$'\n\t'Word-split only on newline and tab, not on spaces
set -xPrint each command before it runs
${PIPESTATUS[@]}Exit status of every command in the last pipeline
set +e / set +uTurn the matching flag back off for a block
command || trueIgnore the exit status of a single command
${VAR:-default}Provide a default for optional variables

Troubleshooting

Script exits silently with no error message
A command is failing under set -e without printing anything. Run the script with bash -x script.sh to trace execution and see which line aborts.

unbound variable on a variable that sometimes is set
Replace the reference with ${VAR:-} to provide an empty default, or ${VAR:-fallback} to provide a meaningful one.

Pipeline fails on a command that is supposed to stop early
Commands like head can cause the producer to receive SIGPIPE, which pipefail treats as a failure. For example, yes | head -n 1 returns 141 (128 plus signal 13). The failure depends on timing: a short producer such as seq 1 10 often finishes before head exits, and the pipeline returns 0. Either redesign the pipeline or wrap the producer in || true when the early exit is expected.

Strict mode breaks a sourced script
The sourced script runs with the caller’s shell options and IFS. If it fails only because of -e, you can put the source call between set +e and set -e. This leaves -u and pipefail enabled and does not change IFS. An unset-variable expansion can still terminate the shell, so fix optional references in the sourced script with ${VAR:-} or another suitable default.

FAQ

Should every Bash script use strict mode?
For scripts that run unattended (cron jobs, CI steps, deployment scripts), yes. For short interactive helpers, the flags are still useful, but the cost of a missed edge case is lower.

Does strict mode work in sh or dash?
set -e and set -u are POSIX, so they work in any compliant shell. set -o pipefail was added to POSIX in the 2024 edition. Bash, Zsh, and Ksh have supported it for years, and dash supports it on Debian 13 and Ubuntu 24.10 and newer. Older dash versions reject it, so use a #!/usr/bin/env bash shebang when the script must run on older systems.

Is set -e really that unreliable?
set -e has well-known edge cases, especially around functions and subshells. It is not a replacement for explicit error handling in critical paths, but combined with pipefail and -u it catches far more bugs than it causes.

How do I pass set -euo pipefail to a one-liner?
Use bash -euo pipefail -c 'your command' when invoking Bash from another program such as ssh or a Makefile.

Conclusion

set -euo pipefail and a tighter IFS catch many of the silent failures that make shell scripts hard to trust. Pair strict mode with clear error handling using Bash functions and a proper shebang line when you want scripts to fail early and predictably. Strict mode is one habit among several, and our Bash best practices guide covers the rest.

Tags

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page