By default, a bash script keeps running even after a command fails. A typo in a variable name expands to an empty string, a failed cd is ignored, and the script marches on as if nothing happened, often doing real damage by the time it stops. The set builtin changes that. With a few flags, you can make bash stop on the first error, treat unset variables as mistakes, and print each command as it runs.
This guide explains the set command and the three options you will use most: set -e, set -u, and set -x, plus pipefail and how to combine them into a strict mode.
What the set Command Does #
set is a shell builtin that turns shell options on or off and sets the positional parameters. For options, it takes this form:
Each option has a short form and a long form. A leading dash turns an option on, and a leading plus turns it off, which is the reverse of what most people expect:
-
set -e enables an option (here, exit on error).
-
set +e disables it.
-
set -o errexit is the long form of set -e.
-
set +o errexit is the long form of set +e.
You place these near the top of a script so they apply to everything that follows, or around a specific block when you only want them in part of the script.
set -e: Exit on Error #
set -e (also written set -o errexit) tells bash to exit immediately if any command returns a non-zero status. It stops a script from continuing past a failure.
Consider a script that changes into a directory and then writes a log message:
#!/bin/bash
set -e
cd /var/cache/myapp
printf 'Cleaning application cache\n'
Without set -e, if the cd fails because the directory does not exist, the script would continue and run the next command in whatever directory it started in. With set -e, the failed cd stops the script before the log message or any later cache command runs.
There are important exceptions where set -e does not trigger an exit, and they trip people up. A command does not cause an exit when it is:
- part of an
if, while, or until test,
- joined with
&& or ||,
- preceded by
!.
This is by design, so that you can test a command’s exit status. If you need a specific command’s failure to be tolerated, append || true:
grep "pattern" file.txt || true
set -u: Treat Unset Variables as Errors #
set -u (or set -o nounset) makes bash exit when you reference a variable that has not been set. This catches one of the most common scripting bugs: a typo in a variable name that silently expands to nothing.
#!/bin/bash
set -u
target_dir="/srv/www"
printf 'Target cache directory: %s\n' "${tagret_dir}/cache"
The variable name is misspelled as tagret_dir. Without set -u, ${tagret_dir} expands to an empty string, and the command prints /cache instead of /srv/www/cache. With set -u, bash stops with an error instead:
deploy.sh: line 5: tagret_dir: unbound variable
When a variable is legitimately optional, provide a default with ${VAR:-default} so the reference is always defined:
echo "Deploying to ${ENVIRONMENT:-staging}"
set -x: Trace Each Command #
set -x (or set -o xtrace) prints every command to standard error before it runs, with the expanded values of any variables. It is the fastest way to see exactly what a script is doing without adding echo statements everywhere.
#!/bin/bash
set -x
name="Linuxize"
echo "Hello, $name"
Running the script shows each command, prefixed with a +, alongside its normal output:
+ name=Linuxize
+ echo 'Hello, Linuxize'
Hello, Linuxize
The trace shows the variable already expanded, so you see the real value bash used. The prefix comes from the PS4 variable. Set it to include the script name and line number, which is invaluable in longer scripts:
PS4='+ ${BASH_SOURCE}:${LINENO}: '
Because tracing is noisy, it is common to enable it only around the section you are debugging with set -x and then turn it off again with set +x.
set -o pipefail: Catch Failures in a Pipeline #
By default, a pipeline returns the exit status of its last command, so a failure earlier in the pipe is hidden. In the pipeline below, wc succeeds even though grep failed because the file does not exist:
grep "ERROR" missing.log | wc -l
Without pipefail, the pipeline exits with the status from wc, which is 0. set -o pipefail changes this so the pipeline returns the status of the rightmost command that failed, or 0 if every command succeeded. Combined with set -e, it makes a failed first stage actually stop the script.
Combine Them into a Strict Mode #
These options are most useful together. The common combination at the top of a careful script is:
#!/bin/bash
set -euo pipefail
This enables exit-on-error, unset-variable checking, and pipeline failure detection in one line. Add x as set -euxo pipefail while debugging, then remove it. This pattern is the foundation of what is often called “bash strict mode”, which our bash strict mode
guide covers in more depth, including the trade-offs of set -e.
Enable and Disable Options for a Block #
You do not have to apply an option to the whole script. Turn one on for a block and off afterward. This is common with tracing:
set -x
deploy_application
sync_assets
set +x
Only the two commands between set -x and set +x are traced. The same pattern works with set -e when you have a section where you want to handle errors manually.
View the Current Options #
Running set -o with no flag prints every option and whether it is on or off. The full list has more than two dozen entries; the four options discussed here are:
errexit off
nounset off
pipefail off
xtrace off
This is a quick way to confirm which options an interactive shell or a sourced script has enabled.
Set Positional Parameters #
The set command has a second job unrelated to options: when given arguments after --, it replaces the positional parameters $1, $2, and so on. This is useful when you want to reset the arguments a script or function reads:
set -- one two three
echo "$2"
The -- marks the end of options so that arguments starting with a dash are not mistaken for flags.
Quick Reference #
For a printable quick reference, see the Bash cheatsheet
.
| Option |
Long form |
Effect |
set -e |
set -o errexit |
Exit immediately on a command failure |
set -u |
set -o nounset |
Error when an unset variable is used |
set -x |
set -o xtrace |
Print each command before running it |
set -o pipefail |
set -o pipefail |
Fail a pipeline if any command in it fails |
set +e |
set +o errexit |
Disable an option (plus instead of dash) |
set -o |
|
List all options and their state |
set -- a b c |
|
Set the positional parameters |
FAQ #
What is the difference between set -e and set -o errexit?
They are the same thing. set -e is the short form and set -o errexit is the long, more readable form. Use whichever you prefer; long forms are common in shared scripts because they are self-documenting.
Why does my script still continue after a command fails with set -e?
The failed command is probably part of an if test, joined with && or ||, or preceded by !. In those positions bash deliberately ignores the failure so you can test exit status. A command substitution or a function call can also mask the failure.
What does the plus sign do, as in set +x?
A plus disables an option that a dash would enable. set -x turns tracing on and set +x turns it off, so you can scope an option to part of a script.
Should I always use set -euo pipefail?
It is a good default for new scripts and catches many bugs early, but set -e in particular has surprising edge cases. For complex scripts, understand how each option behaves and handle the exceptions explicitly rather than assuming the script is fully protected.
Conclusion #
The set builtin turns bash from a forgiving shell into a strict one: set -e stops on errors, set -u catches typos in variable names, and set -x shows you exactly what ran. Combine them as set -euo pipefail at the top of a script, scope tracing to the block you are debugging, and your scripts will fail loudly and early instead of quietly doing the wrong thing. For more on writing reliable scripts, see our guides on bash best practices
and bash functions
.