Lesson 8: Error Handling & Debugging
Goal: Write robust scripts that handle errors gracefully and are easy to debug.
Table of Contents
- Exit Codes
- set Flags — Strict Mode
- trap — Cleanup & Signal Handling
- Error Handling Patterns
- Debugging Techniques
- Exercises
Exit Codes
Every command returns an exit code: 0 = success, 1-255 = error.
ls /etc/passwd
echo $? # 0 (success)
ls /nonexistent
echo $? # 2 (error)
Custom exit codes
#!/bin/bash
validate_input() {
if [[ -z "${1:-}" ]]; then
echo "Error: No input provided" >&2
exit 1
fi
if [[ ! -f "$1" ]]; then
echo "Error: File not found: $1" >&2
exit 2
fi
}
validate_input "$@"
echo "Processing: $1"
Convention
| Code | Meaning |
|---|---|
0 |
Success |
1 |
General error |
2 |
Misuse of command |
126 |
Permission denied |
127 |
Command not found |
128+N |
Killed by signal N |
130 |
Killed by Ctrl+C (SIGINT) |
set Flags — Strict Mode
#!/bin/bash
set -euo pipefail
Handling expected failures with set -e
set -e
# This would exit the script — but maybe you WANT to check:
if grep -q "pattern" file.txt; then
echo "Found"
else
echo "Not found" # This is fine — grep failure is handled
fi
# Or use || true to suppress
count=$(grep -c "missing" file.txt || true)
# Or use || with a handler
rm /protected/file || echo "Could not delete (expected)"
trap — Cleanup & Signal Handling
trap runs a command when the script receives a signal or exits.
Cleanup on exit
#!/bin/bash
set -euo pipefail
TMP_DIR=$(mktemp -d)
cleanup() {
echo "Cleaning up: $TMP_DIR"
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
# Work with temp files — cleanup happens automatically
echo "data" > "$TMP_DIR/file.txt"
echo "Working in $TMP_DIR"
# Even if the script fails, cleanup runs
Catch specific signals
#!/bin/bash
on_sigint() {
echo ""
echo "Caught Ctrl+C — shutting down gracefully..."
exit 130
}
trap on_sigint SIGINT
echo "Running... (Press Ctrl+C to stop)"
while true; do
sleep 1
done
Error handler with line number
#!/bin/bash
set -euo pipefail
on_error() {
echo "Error on line $1 (exit code: $2)" >&2
exit "$2"
}
trap 'on_error ${LINENO} $?' ERR
echo "Line 1: OK"
echo "Line 2: OK"
false # This triggers the error handler
echo "Line 4: Never reached"
Combined trap
#!/bin/bash
set -euo pipefail
TMP_DIR=$(mktemp -d)
LOG_FILE="$TMP_DIR/output.log"
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "Script failed (exit code: $exit_code)" >&2
[[ -f "$LOG_FILE" ]] && echo "Last log entries:" && tail -5 "$LOG_FILE"
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
Error Handling Patterns
Die function
die() {
echo "[FATAL] $*" >&2
exit 1
}
[[ -f "config.yaml" ]] || die "Config file not found"
command -v docker &>/dev/null || die "Docker is required"
Try/catch pattern
try() {
"$@" 2>/dev/null
return $?
}
if try curl -sf https://api.example.com/health; then
echo "API is healthy"
else
echo "API is down"
fi
Retry with backoff
retry_backoff() {
local max_attempts="$1"
shift
local attempt=1
local delay=1
until "$@"; do
if ((attempt >= max_attempts)); then
echo "Failed after $max_attempts attempts" >&2
return 1
fi
echo "Attempt $attempt failed. Retrying in ${delay}s..."
sleep "$delay"
((attempt++))
((delay *= 2)) # exponential backoff
done
}
retry_backoff 5 curl -sf https://api.example.com/health
Assert functions
assert_file_exists() {
[[ -f "$1" ]] || { echo "Assert failed: file '$1' not found" >&2; exit 1; }
}
assert_not_empty() {
[[ -n "$1" ]] || { echo "Assert failed: variable is empty" >&2; exit 1; }
}
assert_command() {
command -v "$1" &>/dev/null || { echo "Assert failed: '$1' not installed" >&2; exit 1; }
}
# Usage
assert_command docker
assert_file_exists ./docker-compose.yml
assert_not_empty "${DB_HOST:-}"
Debugging Techniques
bash -x — Trace execution
bash -x script.sh
Shows every command before it runs:
+ set -euo pipefail
+ NAME=Benjamin
+ echo 'Hello, Benjamin'
Hello, Benjamin
Enable tracing inside a script
#!/bin/bash
set -x # Enable tracing
echo "Step 1"
ls /tmp
set +x # Disable tracing
echo "No more tracing"
Selective debugging
#!/bin/bash
debug() {
[[ "${DEBUG:-false}" == "true" ]] && echo "[DEBUG] $*" >&2
}
debug "Starting script"
debug "Variable FOO=$FOO"
# Run with: DEBUG=true ./script.sh
PS4 — Custom trace prefix
#!/bin/bash
export PS4='+[${BASH_SOURCE}:${LINENO}] '
set -x
echo "hello"
# Output: +[script.sh:5] echo hello
Validate with shellcheck
# Install
sudo apt install shellcheck
# Check a script
shellcheck script.sh
ShellCheck catches common mistakes: unquoted variables, useless cats, deprecated syntax, etc.
Exercises
Exercise 1: Robust script with trap
cat > robust.sh << 'EOF'
#!/bin/bash
set -euo pipefail
TMP=$(mktemp -d)
trap 'echo "Cleanup: removing $TMP"; rm -rf "$TMP"' EXIT
echo "Working in: $TMP"
echo "important data" > "$TMP/data.txt"
echo "File created: $(cat "$TMP/data.txt")"
# Simulate an error
echo "About to fail..."
false
echo "This never runs"
EOF
chmod +x robust.sh && ./robust.sh || true
rm robust.sh
Exercise 2: Error handler with context
cat > errhandler.sh << 'EOF'
#!/bin/bash
set -euo pipefail
on_error() {
echo ""
echo "=== ERROR ===" >&2
echo "Script: $0" >&2
echo "Line: $1" >&2
echo "Code: $2" >&2
echo "=============" >&2
}
trap 'on_error ${LINENO} $?' ERR
echo "Step 1: OK"
echo "Step 2: OK"
echo "Step 3: About to fail..."
cat /this/does/not/exist
echo "Step 4: Never reached"
EOF
chmod +x errhandler.sh && ./errhandler.sh 2>&1 || true
rm errhandler.sh
Key Takeaways
set -euo pipefail— always use strict mode$?— check exit code of last commandtrap 'cleanup' EXIT— auto-cleanup, even on errorstrap 'handler' ERR— custom error reporting with line numbers|| trueto suppress expected failures underset -ebash -x/set -xfor trace debuggingshellcheck— static analysis, catches bugs before runtime- Retry with exponential backoff for network operations
Next Lesson: Lesson 9: Input, Arguments & Options →