Bash Scripting 25 ๐ง Error Handling โ Debugging modes (set -e, set -u, set -x)
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
| Flag | Effect | Common exception |
|---|---|---|
set -e | Exit on non-zero command status | Commands in conditions, ` |
set -u | Exit on unset variable reference | Empty variables are defined; use :- for defaults |
set -o pipefail | Pipeline returns first non-zero status | Bash only, not POSIX sh |
set -x | Print commands after expansion | No behavioral change; output to stderr |
Safe variable forms under set -u
| Form | Behavior when unset |
|---|---|
${var:-default} | Expands to default |
${var-} | Expands to empty string |
${var+set} | Expands to set only if defined |
$var | Exits with error |
Disabling flags
| Command | Effect |
|---|---|
set +e | Disable exit-on-error |
set +u | Disable unset-variable error |
set +o pipefail | Disable pipeline failure propagation |
set +x | Disable 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Pipeline failure not caught | set -e checks only the last command | Add set -o pipefail |
| Script exits on an intentional probe | set -e catches the probe’s failure | Wrap in if, while, or ` |
| Unset optional variable causes exit | set -u treats empty as undefined only if never assigned | Use ${var:-} or ${var+set} |
set -e ignored in a function | The function is called in a condition | Check the function’s return status explicitly |
| Trace too noisy | set -x left on for the whole script | Enable 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
| Item | Value |
|---|---|
set -e | Exit on failing command |
set -u | Exit on unset variable |
set -o pipefail | Pipeline fails on any stage failure |
set -x | Trace commands to stderr |
| Strict mode | set -euo pipefail |
| Safe default form | ${var:-default} |
| Safe test form | ${var+set} |
| Disable tracing | set +x |
| POSIX compatibility | pipefail is Bash-only |
Key takeaways:
set -eis not enough for pipelines. Withoutset -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 -ucatches typos but requires safe forms for optional variables.${var:-default}and${var+set}are the patterns that work underset -uwithout triggering the error.set -ehas exceptions. Commands inif,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 pipefailis three separate flags that the community treats as a unit. Each can be enabled or disabled independently. set -xis for debugging, not production. Enable it conditionally, narrow it to the suspect section, and disable it withset +xwhen done.pipefailis a Bash feature. It is not available in POSIXsh. 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.
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!