Shell Error Handling
Patterns for robust error handling, cleanup, and debugging in shell scripts.
Exit Codes
Standard Exit Codes
Code Meaning
0 Success
1 General error
2 Misuse of shell command
126 Command not executable
127 Command not found
128+N Fatal signal N
130 Ctrl+C (SIGINT)
Checking Exit Status
Check last command's exit status
if ! command; then echo "Command failed with status $?" >&2 exit 1 fi
Alternative pattern
command || { echo "Command failed" >&2 exit 1 }
Capture exit status
command status=$? if (( status != 0 )); then echo "Failed with status $status" >&2 fi
Trap for Cleanup
Basic Cleanup Pattern
#!/usr/bin/env bash set -euo pipefail
cleanup() { local exit_code=$? # Remove temporary files rm -f "$TEMP_FILE" 2>/dev/null || true exit "$exit_code" }
trap cleanup EXIT
TEMP_FILE=$(mktemp)
Script continues...
cleanup runs automatically on exit
Handling Multiple Signals
#!/usr/bin/env bash set -euo pipefail
cleanup() { echo "Cleaning up..." >&2 rm -rf "$WORK_DIR" 2>/dev/null || true }
handle_interrupt() { echo "Interrupted by user" >&2 cleanup exit 130 }
trap cleanup EXIT trap handle_interrupt INT TERM
WORK_DIR=$(mktemp -d)
Trap Best Practices
Preserve original exit code in cleanup
cleanup() { local exit_code=$? # Cleanup operations here rm -f "$temp_file" 2>/dev/null || true # Restore exit code exit "$exit_code" }
Use || true for optional cleanup
trap 'rm -f "$temp_file" 2>/dev/null || true' EXIT
Error Reporting
Standard Error Output
Always write errors to stderr
echo "Error: Something went wrong" >&2
Error function
error() { echo "Error: $*" >&2 }
Die function - error and exit
die() { echo "Fatal: $*" >&2 exit 1 }
Usage
[[ -f "$config" ]] || die "Config file not found: $config"
Verbose Logging
#!/usr/bin/env bash set -euo pipefail
VERBOSE="${VERBOSE:-false}"
log() { if [[ "$VERBOSE" == "true" ]]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2 fi }
error() { echo "[ERROR] $*" >&2 }
log "Starting script" log "Processing file: $file"
Defensive Programming
Check Prerequisites
Check required commands exist
require_command() { command -v "$1" >/dev/null 2>&1 || { echo "Error: Required command '$1' not found" >&2 exit 1 } }
require_command jq require_command curl require_command shellcheck
Validate Input
Validate arguments
if [[ $# -lt 2 ]]; then echo "Usage: $0 <source> <destination>" >&2 exit 1 fi
source_file="$1" dest_dir="$2"
Validate file exists
[[ -f "$source_file" ]] || { echo "Error: Source file not found: $source_file" >&2 exit 1 }
Validate directory
[[ -d "$dest_dir" ]] || { echo "Error: Destination directory not found: $dest_dir" >&2 exit 1 }
Safe Temporary Files
Create secure temp file
TEMP_FILE=$(mktemp) || { echo "Error: Failed to create temp file" >&2 exit 1 }
Create secure temp directory
TEMP_DIR=$(mktemp -d) || { echo "Error: Failed to create temp directory" >&2 exit 1 }
Always clean up
trap 'rm -rf "$TEMP_FILE" "$TEMP_DIR" 2>/dev/null || true' EXIT
Debugging
Debug Mode
#!/usr/bin/env bash
Enable debug mode via environment variable
if [[ "${DEBUG:-}" == "1" ]]; then set -x fi
set -euo pipefail
Or toggle with a flag
while getopts "d" opt; do case $opt in d) set -x ;; *) echo "Usage: $0 [-d]" >&2; exit 1 ;; esac done
Trace Execution
Enable tracing for specific section
set -x problematic_code set +x
Trace with custom PS4
export PS4='+ ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' set -x
Error Recovery Patterns
Retry Pattern
retry() { local max_attempts="${1:-3}" local delay="${2:-1}" shift 2 local cmd=("$@")
local attempt=1
while (( attempt <= max_attempts )); do
if "${cmd[@]}"; then
return 0
fi
echo "Attempt $attempt failed, retrying in ${delay}s..." >&2
sleep "$delay"
(( attempt++ ))
done
echo "All $max_attempts attempts failed" >&2
return 1
}
Usage
retry 3 5 curl -f "http://example.com/api"
Fallback Pattern
Try primary, fall back to secondary
get_config() { if [[ -f "$HOME/.config/myapp/config" ]]; then cat "$HOME/.config/myapp/config" elif [[ -f "/etc/myapp/config" ]]; then cat "/etc/myapp/config" else echo "Error: No config file found" >&2 return 1 fi }