11.2. Options & Traps for Error Handling#
11.2.1. Common Pitfalls#
1. Forgetting that set -e doesn’t catch all errors
# These DON'T trigger set -e:
if some_command; then # Error ignored in if condition
echo "OK"
fi
some_command || true # Error suppressed with ||
! some_command # Error suppressed with negation
# Fix: Use explicit checks or avoid the pattern
if ! some_command; then
echo "Command failed" >&2
return 1
fi
2. Trap handlers that don’t preserve exit codes
# Bad: Overwrites exit code
trap 'echo "Error!"' ERR
command_that_fails
echo $? # Prints exit code of echo, not the command!
# Good: Preserve the exit code
trap 'echo "Error! (code: $?)"' ERR
3. Not unsetting traps when they’re no longer needed
# Bad: Inner function's trap persists
outer() {
trap 'echo "Outer trap"' EXIT
inner
}
inner() {
trap 'echo "Inner trap"' EXIT
# When inner exits, both traps run!
}
# Better: Unset when done
inner() {
trap 'echo "Inner trap"' EXIT
# ... do work ...
trap - EXIT # Remove the trap
}
4. Using set -e with functions that return non-zero intentionally
# Problem: This function returns error intentionally for testing
check_status() {
[[ "$1" == "ok" ]] && return 0 || return 1
}
set -e
check_status "bad" # Script EXITS here!
# Fix: Use || to handle expected failures
set -e
check_status "bad" || echo "Status check failed"
11.2.2. Real-World Example: Production-Grade Script#
#!/bin/bash
# Production script with comprehensive error handling
set -euo pipefail
# Configuration
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/myscript.log"
readonly LOCK_FILE="/var/run/myscript.lock"
# Error handler with logging
error_exit() {
local line_number=$1
local error_code=$2
{
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Command failed at line $line_number"
echo "Exit code: $error_code"
echo "Command: $BASH_COMMAND"
} | tee -a "$LOG_FILE" >&2
exit "$error_code"
}
# Cleanup on exit
cleanup() {
local exit_code=$?
# Remove lock file
rm -f "$LOCK_FILE"
# Log completion
if [[ $exit_code -eq 0 ]]; then
echo "[$(date)] Script completed successfully" >> "$LOG_FILE"
else
echo "[$(date)] Script failed with code $exit_code" >> "$LOG_FILE"
fi
return $exit_code
}
# Install handlers
trap 'error_exit $LINENO $?' ERR
trap cleanup EXIT
# Prevent concurrent execution
if [[ -f "$LOCK_FILE" ]]; then
echo "Script already running (PID: $(cat $LOCK_FILE))" >&2
exit 1
fi
echo $$ > "$LOCK_FILE"
# Main work
echo "[$(date)] Starting main work" >> "$LOG_FILE"
do_work || return $?
echo "[$(date)] Work completed" >> "$LOG_FILE"
11.2.3. Practical Trap Examples#
11.2.3.1. Error Handler with Context#
#!/bin/bash
set -euo pipefail
handle_error() {
local line_number=$1
local error_code=$2
echo "Error on line $line_number: Command exited with code $error_code" >&2
# Log to syslog for production
logger -t myscript "Error at line $line_number (code $error_code)"
# Send alert email
echo "Script failed at line $line_number" | \
mail -s "Alert: myscript failed" admin@example.com
}
trap 'handle_error $LINENO $?' ERR
# Script continues...
11.2.3.2. Graceful Shutdown#
#!/bin/bash
# Start a background process
long_running_job &
job_pid=$!
handle_interrupt() {
echo "Interrupted! Cleaning up..."
kill $job_pid 2>/dev/null || true
exit 130 # 128 + SIGINT(2)
}
trap handle_interrupt INT TERM
wait $job_pid
echo "Job completed successfully"
11.2.3.3. Debug Tracing#
#!/bin/bash
set -x # Enable tracing
debug_trace() {
echo "[DEBUG] Command: $BASH_COMMAND"
echo "[DEBUG] Line: $LINENO"
}
trap debug_trace DEBUG
# All commands are printed before execution
ls -la
grep pattern file.txt
11.2.4. The trap Command for Cleanup#
trap runs code when a signal is received or script exits:
11.2.4.1. Basic trap Syntax#
trap 'cleanup_function' EXIT
trap 'handle_error' ERR
trap 'handle_interrupt' INT
trap 'handle_term' TERM
trap 'handle_debug' DEBUG
11.2.4.2. Common Signals to Trap#
Signal |
When |
Usage |
|---|---|---|
|
Script exits (any reason) |
Cleanup resources |
|
Error detected |
Error logging |
|
Ctrl+C pressed |
Graceful shutdown |
|
Termination signal |
Clean exit |
|
After each command |
Debugging/tracing |
11.2.4.3. Cleanup Pattern#
#!/bin/bash
# Create temporary resources
temp_file=$(mktemp)
pid_file="/tmp/daemon.pid"
# Cleanup function
cleanup() {
local exit_code=$?
rm -f "$temp_file"
rm -f "$pid_file"
echo "Cleaned up at $(date)"
exit $exit_code
}
trap cleanup EXIT
# Script continues...
echo "Working..." > "$temp_file"
11.2.5. The set Command for Error Control#
The set command modifies shell behavior to catch errors early:
11.2.5.1. Key set Options#
Option |
Short |
Effect |
|---|---|---|
|
Exit on any error |
|
|
Error on undefined variables |
|
|
Print commands before execution (debugging) |
|
|
Pipeline fails if any command fails |
|
|
Prevent file overwriting with |
|
|
Same as noclobber |
11.2.5.2. Basic set -e Usage#
#!/bin/bash
set -e # Exit on any error
echo "Step 1"
ls /nonexistent # Script EXITS here
echo "Step 2" # Never executes
11.2.5.3. set -u to Catch Typos#
#!/bin/bash
set -u # Error on undefined variables
name="Alice"
echo $name # OK
echo $Name # ERROR: unbound variable
11.2.5.4. Combining Options#
#!/bin/bash
set -euo pipefail # Best practice: strict mode
# Now:
# - Script exits on any error
# - Script errors on undefined variables
# - Pipeline fails if any command fails