| |

Bash Scripting 25 ๐Ÿง Error Handling โ€” Debugging modes (set -e, set -u, set -x)

โฑ๏ธ Estimated read: 12 min | |

Bash is permissive by default. A command that fails does not stop the script. A reference to an unset variable expands to an empty string. A pipeline returns the exit status of its last command, masking failures in every earlier stage. These defaults are convenient for interactive use and dangerous for scripts that perform multi-step operations, because a script can continue past a failure and produce a partially applied result that looks like success.

The set builtin provides the switches that change these defaults. set -e exits on a failing command. set -u treats an unset variable as an error. set -o pipefail makes a pipeline fail if any stage fails, not just the last one. set -x prints each command before executing it, which is the primary tool for tracing a script that is behaving unexpectedly. Together, the first three form what the Bash community calls “strict mode” .

Key point: Strict mode is not a single flag. It is a combination of set -e, set -u, and set -o pipefail, and each one has exceptions and edge cases. Enable them deliberately and know where they do not apply.


Why these modes exist

The silent failure problem. Without set -e, a script that runs cd /nonexistent continues executing in the original directory. Every subsequent relative path resolves against the wrong location. The script may create files in the wrong place, delete the wrong data, or report success while doing nothing. set -e makes the failed cd terminate the script, which is usually the correct behavior for a sequence of dependent steps .

The pipeline masking problem. In a standard pipeline, the shell returns the exit status of the last command. If generate_backup_stream.sh | gzip > backup.tar.gz is run and the generator fails but gzip succeeds, the pipeline returns 0. The script believes the backup succeeded. set -o pipefail changes the pipeline’s return status to the first non-zero status among its stages, allowing set -e to catch the failure .

The typo problem. A script that references ${HSOTNAME} instead of ${HOSTNAME} expands to an empty string without set -u. The command that uses the empty value may fail with a confusing error, or may succeed with the wrong data. set -u makes the reference itself an error, which points directly at the typo .

The debugging problem. When a script fails, the error message identifies the command that failed but not the sequence of commands that led to it. set -x prints each command after expansion, so the trace shows the actual values that were used, including the result of variable substitutions . It is the fastest way to see why a conditional branch was taken or why a command received the wrong argument.


a. set -e: exit on error

set -e instructs the shell to exit immediately when a command returns a non-zero status. It is the foundation of fail-fast scripting.

#!/bin/bash
set -e

cd /nonexistent    # fails, script exits here
echo "This line never runs"

The flag has well-known exceptions. A command that is part of a condition is not subject to set -e: if failing_command; then, while failing_command; do, and failing_command || true all suppress the exit. A command in a pipeline other than the last is not checked without pipefail. A command whose failure is explicitly handled with || is not considered a failure .

# These do not trigger set -e
if grep -q "pattern" file.txt; then
    echo "found"
fi

# The || true makes the failure explicit and acceptable
rm -f /tmp/maybe-exists || true

The flag is useful for scripts that perform a sequence of dependent operations, where a failure at any step makes the remaining steps meaningless or dangerous. It is less useful for scripts that intentionally probe for conditions, because every probe must be wrapped in a conditional or an || true.

b. set -u: error on unset variables

set -u causes the shell to treat an unset variable reference as an error and exit. It catches typos and missing environment variables before they propagate into commands.

#!/bin/bash
set -u

echo "$HOSTNAME"    # set by the shell, works
echo "$HSOTNAME"    # unset, script exits with an error

The flag does not affect variables that are set but empty. var="" is defined, and $var expands to nothing without triggering set -u. The flag only catches references to names that have never been assigned. To check whether an optional variable is set, use the ${var:-default} form or the ${var+set} test, both of which are safe under set -u .

# Safe under set -u
echo "${UNSET_VAR:-default}"       # prints "default"
echo "${UNSET_VAR-}"               # prints empty string

# Unsafe
echo "$UNSET_VAR"                  # exits with error

c. set -o pipefail: fail on any pipeline stage

set -o pipefail changes the exit status of a pipeline from “the status of the last command” to “the status of the last command that failed.” If every command succeeds, the pipeline returns 0. If any command fails, the pipeline returns that command’s non-zero status .

#!/bin/bash
set -e
set -o pipefail

# Without pipefail: if generator fails but gzip succeeds, pipeline returns 0
# With pipefail: pipeline returns generator's failure status, set -e exits
generate_backup_stream.sh | gzip -9 > /backup/archive.tar.gz

The flag is a Bash feature and is not available in POSIX sh . It is essential for any script that uses pipelines for data processing, because the default behavior silently discards failures in every stage except the last.

d. set -x: trace execution

set -x prints each command to standard error after expansion, prefixed with +. It does not change the script’s behavior; it only makes the execution visible.

#!/bin/bash
set -x

name="world"
echo "Hello, $name"

The trace shows the expanded command:

+ name=world
+ echo 'Hello, world'
Hello, world

The flag is typically enabled conditionally, through a -d option or an environment variable, so that normal runs are quiet and debug runs produce a trace.

if [[ "${DEBUG:-}" == "1" ]]; then
    set -x
fi

The trace can be noisy for long scripts. It is most useful when narrowed to a specific section by enabling it before the suspect code and disabling it with set +x afterward.


Complete Example Session

# ============================================
# PART 1: THE SILENT FAILURE
# ============================================
#!/bin/bash

cd /nonexistent
echo "Still running in $(pwd)"
# The cd fails, but the script continues.
# The echo runs in the original directory.
# ============================================
# PART 2: SET -E
# ============================================
#!/bin/bash
set -e

cd /nonexistent
echo "Never reached"
# The script exits immediately when cd fails.
# ============================================
# PART 3: SET -E EXCEPTIONS
# ============================================
#!/bin/bash
set -e

# A command in an if condition does not trigger the exit
if grep -q "pattern" /nonexistent/file; then
    echo "found"
fi

# The script continues past the failed grep.
# ============================================
# PART 4: SET -U
# ============================================
#!/bin/bash
set -u

echo "$HOSTNAME"      # works, HOSTNAME is set by the shell
echo "$HSOTNAME"      # exits with "HSOTNAME: unbound variable"
# ============================================
# PART 5: SAFE DEFAULTS UNDER SET -U
# ============================================
#!/bin/bash
set -u

# Use :- for a default value
port="${PORT:-8080}"

# Use ${var+set} to test if a variable is defined
if [[ -n "${OPTIONAL_VAR+set}" ]]; then
    echo "OPTIONAL_VAR is defined"
fi
# ============================================
# PART 6: SET -O PIPEFAIL
# ============================================
#!/bin/bash
set -e
set -o pipefail

# If generator fails but gzip succeeds:
# - without pipefail: pipeline returns 0, script continues
# - with pipefail: pipeline returns failure, set -e exits
generate_data | gzip > output.gz
# ============================================
# PART 7: DEMONSTRATING PIPEFAIL
# ============================================
#!/bin/bash
set -e
set -o pipefail

false | true
echo "Never reached with pipefail"
# The false command fails, pipefail propagates it,
# and set -e exits the script.
# ============================================
# PART 8: SET -X FOR TRACING
# ============================================
#!/bin/bash
set -x

name="world"
echo "Hello, $name"
# Output:
+ name=world
+ echo 'Hello, world'
Hello, world
# ============================================
# PART 9: CONDITIONAL TRACING
# ============================================
#!/bin/bash

if [[ "${DEBUG:-}" == "1" ]]; then
    set -x
fi

# ... script code ...

set +x   # disable tracing
# ============================================
# PART 10: COMPLETE STRICT MODE SCRIPT
# ============================================
#!/bin/bash
set -euo pipefail

# set -e: exit on error
# set -u: error on unset variable
# set -o pipefail: fail if any pipeline stage fails

input="${1:-}"
[[ -z "$input" ]] && { echo "Usage: $0 <file>" >&2; exit 1; }

grep -q "pattern" "$input" | cat
echo "Pattern found"

The ten parts covered the silent failure, set -e, the exceptions to set -e, set -u, safe defaults under set -u, set -o pipefail, a demonstration of pipefail, set -x tracing, conditional tracing, and the complete strict mode script.


Quick Reference

The flags

FlagEffectCommon exception
set -eExit on non-zero command statusCommands in conditions, `
set -uExit on unset variable referenceEmpty variables are defined; use :- for defaults
set -o pipefailPipeline returns first non-zero statusBash only, not POSIX sh
set -xPrint commands after expansionNo behavioral change; output to stderr

Safe variable forms under set -u

FormBehavior when unset
${var:-default}Expands to default
${var-}Expands to empty string
${var+set}Expands to set only if defined
$varExits with error

Disabling flags

CommandEffect
set +eDisable exit-on-error
set +uDisable unset-variable error
set +o pipefailDisable pipeline failure propagation
set +xDisable tracing

Best Practices

โœ… Do This:

# Enable strict mode at the top of the script
set -euo pipefail                                          # โœ…
# Use :- for optional variables under set -u
port="${PORT:-8080}"                                       # โœ…
# Wrap probes in conditionals or || true
if grep -q "x" file; then ... fi                           # โœ…
# Enable tracing conditionally
[[ "${DEBUG:-}" == "1" ]] && set -x                        # โœ…

โŒ Don’t Do This:

# Assume set -e catches pipeline failures
set -e
false | true    # pipeline returns 0, script continues      # โŒ
# Reference optional variables without a default
echo "$OPTIONAL"                                           # โŒ (under set -u)
# Use set -e in scripts that intentionally probe
# Every probe would need a conditional                    # โŒ
# Leave set -x enabled in production
set -x  # noisy, leaks variable values to stderr           # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Pipeline failure not caughtset -e checks only the last commandAdd set -o pipefail
Script exits on an intentional probeset -e catches the probe’s failureWrap in if, while, or `
Unset optional variable causes exitset -u treats empty as undefined only if never assignedUse ${var:-} or ${var+set}
set -e ignored in a functionThe function is called in a conditionCheck the function’s return status explicitly
Trace too noisyset -x left on for the whole scriptEnable only around the suspect code

Real-World Examples

1. Strict mode header

set -euo pipefail

2. Conditional exit-on-error

set -e
cd /required/dir

3. Safe default for optional variable

output="${OUTPUT:-/tmp/result}"

4. Pipeline failure detection

set -o pipefail
producer | consumer

5. Intentional probe with set -e

if ! command -v git &>/dev/null; then
    echo "git not found" >&2
    exit 1
fi

6. Allow a specific failure

rm -f /tmp/lock || true

7. Enable tracing conditionally

[[ "${DEBUG:-}" == "1" ]] && set -x

8. Disable tracing after a section

set +x

9. Test if a variable is defined

[[ -n "${VAR+set}" ]] && echo "VAR is defined"

10. Full strict mode script

#!/bin/bash
set -euo pipefail
input="${1:-}"
[[ -z "$input" ]] && { echo "Usage: $0 <file>" >&2; exit 1; }

Visual

set -e and pipeline masking

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PIPELINE WITHOUT PIPEFAIL                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  generate | gzip > backup.gz                                 โ”‚
โ”‚      โ”‚          โ”‚                                            โ”‚
โ”‚      โ–ผ          โ–ผ                                            โ”‚
โ”‚   FAILS       SUCCEEDS                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Pipeline status = status of last command = 0               โ”‚
โ”‚  set -e sees 0, script continues.                            โ”‚
โ”‚  Backup is incomplete, but the script reports success.      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PIPELINE WITH PIPEFAIL                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  generate | gzip > backup.gz                                 โ”‚
โ”‚      โ”‚          โ”‚                                            โ”‚
โ”‚      โ–ผ          โ–ผ                                            โ”‚
โ”‚   FAILS       SUCCEEDS                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Pipeline status = first non-zero = generator's failure     โ”‚
โ”‚  set -e sees non-zero, script exits.                         โ”‚
โ”‚  The failure is visible.                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

set -u and safe variable forms

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  UNDER SET -U                                                โ”‚
โ”‚                                                              โ”‚
โ”‚  echo "$VAR"                                                 โ”‚
โ”‚    VAR never assigned โ”€โ”€โ–บ error, exit                        โ”‚
โ”‚                                                              โ”‚
โ”‚  echo "${VAR:-default}"                                      โ”‚
โ”‚    VAR never assigned โ”€โ”€โ–บ prints "default"                   โ”‚
โ”‚                                                              โ”‚
โ”‚  VAR=""; echo "$VAR"                                         โ”‚
โ”‚    VAR assigned empty โ”€โ”€โ–บ prints empty, no error             โ”‚
โ”‚                                                              โ”‚
โ”‚  [[ -n "${VAR+set}" ]]                                       โ”‚
โ”‚    VAR never assigned โ”€โ”€โ–บ condition false                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

set -x trace

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SET -X TRACE                                                โ”‚
โ”‚                                                              โ”‚
โ”‚  Script:                                                     โ”‚
โ”‚    name="world"                                              โ”‚
โ”‚    echo "Hello, $name"                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Output to stderr:                                           โ”‚
โ”‚    + name=world                                              โ”‚
โ”‚    + echo 'Hello, world'                                     โ”‚
โ”‚                                                              โ”‚
โ”‚  The + prefix marks each executed command.                   โ”‚
โ”‚  Variable expansions are shown in their final form.          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Strict mode decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WHEN TO USE STRICT MODE                                     โ”‚
โ”‚                                                              โ”‚
โ”‚  Use set -euo pipefail when:                                 โ”‚
โ”‚    Script performs sequential dependent operations           โ”‚
โ”‚    A failure at any step makes the rest meaningless          โ”‚
โ”‚    You want failures to stop the script immediately          โ”‚
โ”‚                                                              โ”‚
โ”‚  Do not use set -e when:                                     โ”‚
โ”‚    Script intentionally probes for conditions                โ”‚
โ”‚    Every command's failure is expected and handled           โ”‚
โ”‚    You cannot wrap every probe in a conditional              โ”‚
โ”‚                                                              โ”‚
โ”‚  set -u and pipefail are safe in almost all scripts.         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
set -eExit on failing command
set -uExit on unset variable
set -o pipefailPipeline fails on any stage failure
set -xTrace commands to stderr
Strict modeset -euo pipefail
Safe default form${var:-default}
Safe test form${var+set}
Disable tracingset +x
POSIX compatibilitypipefail is Bash-only

Key takeaways:

  • set -e is not enough for pipelines. Without set -o pipefail, a pipeline returns the status of its last command, masking failures in every earlier stage. The two flags are almost always used together .
  • set -u catches typos but requires safe forms for optional variables. ${var:-default} and ${var+set} are the patterns that work under set -u without triggering the error.
  • set -e has exceptions. Commands in if, while, and || expressions do not trigger the exit. A script that intentionally probes for conditions must wrap those probes or the flag will terminate it unexpectedly.
  • Strict mode is a convention, not a builtin. set -euo pipefail is three separate flags that the community treats as a unit. Each can be enabled or disabled independently.
  • set -x is for debugging, not production. Enable it conditionally, narrow it to the suspect section, and disable it with set +x when done.
  • pipefail is a Bash feature. It is not available in POSIX sh. Scripts that must be portable cannot rely on it.

Remember: Bash’s defaults prioritize continuing over failing. For interactive use, that is the right choice. For scripts that perform dependent operations, it is the wrong choice. set -euo pipefail changes the defaults to fail fast, but each flag has exceptions and edge cases. Know when a command is in a condition, know when a pipeline is masking a failure, and know how to reference a variable that might not be set. Strict mode is not a substitute for thinking about failure; it is a tool for making failures visible.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!