Stage 2 · Tools
Scripting Patterns
Error Handling Patterns
set -euo pipefail, error functions, cleanup traps, and building resilient scripts.
Strict Mode
Strict mode catches errors at the point they occur rather than letting them propagate silently. Every production script should start with strict mode.
#!/usr/bin/env bash
# Exit on any command failure
set -e
# Error on undefined variables
set -u
# Pipeline returns exit code of last failed command
set -o pipefail
# Combine into one line (common pattern)
set -euo pipefailset -e catches failed commands. set -u catches undefined variables. set -o pipefail catches failures in pipelines. Together they catch the three most common shell script bug categories.
# Without strict mode: these errors are silent
# set -e: catches this
false
echo "This never runs"
# set -u: catches this
echo "$UNDEFINED_VAR"
# set -o pipefail: catches this
false | true
echo "Without pipefail, this runs despite the false"Without strict mode, all three of these errors are silent — the script continues as if nothing happened. Strict mode makes these errors terminate the script.
Error Functions
Consistent error handling requires standardized error functions. These provide uniform formatting, logging, and exit behavior.
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="$(basename "{BASH_SOURCE[0]}")"
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"
}
die() {
log_error "$@"
exit 1
}
# Usage
validate_input() {
[[ -z "{1:-}" ]] && die "Missing required argument"
[[ -f "$1" ]] || die "File not found: $1"
[[ -r "$1" ]] || die "File not readable: $1"
}die() logs an error and exits. log_error() and log_info() provide consistent formatting. These functions centralize error handling — change the behavior in one place, affect all error paths.
Cleanup Traps
#!/usr/bin/env bash
set -euo pipefail
TMPDIR=""
LOCK_FILE=""
BACKGROUND_PIDS=()
cleanup() {
local exit_code=$?
# Remove temp directory
[[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
# Remove lock file
[[ -n "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
# Kill background processes
for pid in "${BACKGROUND_PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
done
if (( exit_code != 0 )); then
log_error "Script failed with exit code $exit_code"
fi
exit "$exit_code"
}
trap cleanup EXIT
trap 'trap - INT; kill -INT $$' INT
trap 'trap - TERM; kill -TERM $$' TERM
# Initialize resources
TMPDIR=$(mktemp -d)
LOCK_FILE="/tmp/$(basename "$0").lock"
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
die "Another instance is running"
fiThe EXIT trap runs on any exit. It cleans up temp files, lock files, and background processes. The INT and TERM traps forward signals to child processes and then let EXIT run for cleanup.
Error Recovery
#!/usr/bin/env bash
set -euo pipefail
# Retry with exponential backoff
retry() {
local max_attempts="$1"
local delay=2
shift
for ((attempt=1; attempt<=max_attempts; attempt++)); do
if "$@"; then
return 0
fi
if (( attempt < max_attempts )); then
log_info "Attempt $attempt failed, retrying in {delay}s..."
sleep "$delay"
delay=$((delay * 2))
fi
done
log_error "All $max_attempts attempts failed for: $*"
return 1
}
# Safe command execution
safe_exec() {
local description="$1"
shift
log_info "Running: $description"
if ! "$@"; then
log_error "Failed: $description"
return 1
fi
}
# Rollback on failure
deploy_with_rollback() {
local backup_id
backup_id=$(create_backup)
if ! perform_deploy; then
log_error "Deploy failed, rolling back..."
restore_backup "$backup_id"
die "Deploy failed and rolled back"
fi
log_info "Deploy successful"
cleanup_backup "$backup_id"
}retry() attempts a command multiple times with exponential backoff. safe_exec() wraps commands with error logging. deploy_with_rollback() demonstrates the rollback pattern for critical operations.
Production Error Handling
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "{BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "{BASH_SOURCE[0]}")"
TMPDIR=""
VERBOSE=false
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2; }
die() { log_error "$@"; exit 1; }
cleanup() {
local rc=$?
[[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
(( rc != 0 )) && log_error "Exiting with code $rc"
exit "$rc"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
main() {
TMPDIR=$(mktemp -d)
# Your script logic here
log "Script completed successfully"
}
main "$@"This template includes: strict mode, logging functions, cleanup trap, signal handling, and main function. Adapt it for your needs. Every production script should have at least this structure.
set -e does not apply to commands in conditionals (if, while, ||, &&). It also does not apply to commands in subshells. Understand these edge cases to avoid surprises.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.