Error Handling
Production scripts must handle errors gracefully. A script that silently continues after a failure can cause serious damage. This lesson teaches you to catch errors early, clean up properly, and provide meaningful feedback when things go wrong!
The set Options
Bash's set builtin enables critical error-handling behaviors.
Click Run to execute your code
Essential Set Options
| Option | Effect | Why Use It |
|---|---|---|
set -e |
Exit on error | Stops script when any command fails |
set -u |
Exit on undefined variable | Catches typos and missing variables |
set -o pipefail |
Pipeline returns rightmost error | Catches errors in pipelines |
set -E |
ERR trap inherited by functions | Error handlers work in functions |
#!/usr/bin/env bash
set -euo pipefail
This catches most common errors automatically.
Exit Codes
Every command returns an exit code. Use them to detect and communicate errors.
# Standard exit codes
# 0 - Success
# 1 - General error
# 2 - Misuse of command
# 126 - Permission denied
# 127 - Command not found
# 128+N - Fatal error signal N
# Check exit code with $?
command
if [[ $? -ne 0 ]]; then
echo "Command failed"
fi
# Better: use if directly
if ! command; then
echo "Command failed"
fi
# Define custom exit codes
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_USAGE=2
readonly EXIT_CONFIG=3
die() {
echo "ERROR: $*" >&2
exit "${EXIT_ERROR}"
}
# Return specific codes
validate_config() {
[[ -f "$config" ]] || return $EXIT_CONFIG
return $EXIT_SUCCESS
}
# Check specific failure
if ! validate_config; then
case $? in
$EXIT_CONFIG) echo "Configuration error" ;;
*) echo "Unknown error" ;;
esac
fi
The trap Command
Use trap to execute cleanup code when your script exits or receives signals.
Click Run to execute your code
Common Trap Patterns
# Cleanup on exit (any exit)
cleanup() {
rm -f "$temp_file"
echo "Cleanup complete"
}
trap cleanup EXIT
# Handle specific signals
trap 'echo "Interrupted!"; exit 130' INT
trap 'echo "Terminated!"; exit 143' TERM
# Error handler
on_error() {
local line="$1"
local code="$2"
echo "Error on line $line: exit code $code" >&2
}
trap 'on_error ${LINENO} $?' ERR
# Multiple cleanup actions
declare -a cleanup_tasks=()
add_cleanup() {
cleanup_tasks+=("$1")
}
run_cleanup() {
for task in "${cleanup_tasks[@]}"; do
eval "$task" || true # Continue even if cleanup fails
done
}
trap run_cleanup EXIT
# Usage
temp_file=$(mktemp)
add_cleanup "rm -f '$temp_file'"
temp_dir=$(mktemp -d)
add_cleanup "rm -rf '$temp_dir'"
Common Signals
| Signal | Number | Cause |
|---|---|---|
EXIT |
N/A | Any script exit (normal or error) |
ERR |
N/A | Command returns non-zero |
INT |
2 | Ctrl+C (interrupt) |
TERM |
15 | Termination request |
HUP |
1 | Terminal closed |
Error Recovery Patterns
Sometimes you want to handle errors gracefully rather than exit immediately.
# Retry with backoff
retry() {
local max_attempts=${1:-3}
local delay=${2:-1}
local cmd="${@:3}"
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if eval "$cmd"; then
return 0
fi
echo "Attempt $attempt failed, retrying in ${delay}s..." >&2
sleep "$delay"
((attempt++))
((delay *= 2)) # Exponential backoff
done
echo "All $max_attempts attempts failed" >&2
return 1
}
# Usage
retry 3 2 curl -sf "https://api.example.com/health"
# Try/catch pattern
try() {
local result
result=$("$@" 2>&1) && echo "$result" || {
echo "Error: $result" >&2
return 1
}
}
# Fallback chain
get_config() {
cat "$1" 2>/dev/null ||
cat "$HOME/.config/app.conf" 2>/dev/null ||
cat "/etc/app/default.conf" 2>/dev/null ||
echo "default_value"
}
# Conditional error handling
set +e # Temporarily disable exit-on-error
risky_command
status=$?
set -e # Re-enable
if [[ $status -ne 0 ]]; then
handle_error $status
fi
set +e sparingly and only for specific commands. Always re-enable with set -e immediately after.
Assertion Functions
Build reusable assertion functions for common validation patterns.
# Assertion library
assert_not_empty() {
local name="$1"
local value="$2"
[[ -n "$value" ]] || die "Assertion failed: $name must not be empty"
}
assert_file_exists() {
local file="$1"
[[ -f "$file" ]] || die "Assertion failed: File not found: $file"
}
assert_directory_exists() {
local dir="$1"
[[ -d "$dir" ]] || die "Assertion failed: Directory not found: $dir"
}
assert_command_exists() {
local cmd="$1"
command -v "$cmd" &>/dev/null ||
die "Assertion failed: Command not found: $cmd"
}
assert_root() {
[[ $EUID -eq 0 ]] || die "This script must be run as root"
}
# Usage
main() {
assert_command_exists "curl"
assert_file_exists "$config_file"
assert_not_empty "API_KEY" "$API_KEY"
# Script continues only if all assertions pass
}
Common Mistakes
1. Not checking command success
# Wrong - continues even if cd fails!
cd /nonexistent
rm -rf * # Deletes from wrong directory!
# Correct
cd /nonexistent || exit 1
rm -rf *
# Or with set -e
set -e
cd /nonexistent # Script exits here
2. Ignoring pipeline errors
# Wrong - only checks last command
cat missing.txt | grep pattern
echo "Status: $?" # Shows 0 or 1 from grep, not cat!
# Correct - use pipefail
set -o pipefail
cat missing.txt | grep pattern # Now fails if cat fails
3. Not cleaning up on error
# Wrong - temp file left behind on error
temp=$(mktemp)
process_data > "$temp" # If this fails, temp remains
rm "$temp"
# Correct - trap ensures cleanup
temp=$(mktemp)
trap "rm -f '$temp'" EXIT
process_data > "$temp"
# temp is removed even on error
Exercise: Robust File Processor
Task: Create a script with comprehensive error handling!
Requirements:
- Use
set -euo pipefail - Validate input file exists
- Use trap for cleanup
- Provide meaningful error messages
- Return appropriate exit codes
Show Solution
#!/usr/bin/env bash
set -euo pipefail
# Exit codes
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_USAGE=2
# Globals
temp_file=""
# Cleanup handler
cleanup() {
[[ -n "$temp_file" && -f "$temp_file" ]] && rm -f "$temp_file"
}
trap cleanup EXIT
# Error handler
on_error() {
echo "ERROR: Script failed at line $1" >&2
}
trap 'on_error $LINENO' ERR
# Utility functions
die() {
echo "ERROR: $*" >&2
exit $EXIT_ERROR
}
usage() {
echo "Usage: $0 "
exit $EXIT_USAGE
}
# Validation
validate_input() {
local file="$1"
[[ -n "$file" ]] || die "Input file required"
[[ -f "$file" ]] || die "File not found: $file"
[[ -r "$file" ]] || die "File not readable: $file"
}
# Main processing
process_file() {
local input="$1"
temp_file=$(mktemp) || die "Failed to create temp file"
echo "Processing: $input"
# Simulate processing
wc -l "$input" > "$temp_file"
cat "$temp_file"
echo "Processing complete"
}
# Entry point
main() {
[[ $# -eq 1 ]] || usage
validate_input "$1"
process_file "$1"
exit $EXIT_SUCCESS
}
main "$@"
Summary
- set -euo pipefail: Essential trio for catching errors automatically
- Exit Codes: Use meaningful codes (0=success, non-zero=error types)
- trap EXIT: Always clean up temporary files and resources
- trap ERR: Add error context with line numbers
- Assertions: Validate preconditions early with clear messages
- Recovery: Implement retry logic for transient failures
What's Next?
Good error handling needs good Logging. Next, you'll learn to implement logging functions with levels, timestamps, and proper output handling!
Enjoying these tutorials?