🔧 Bash Error Handling & Debugging

Comprehensive Reference Guide — set options, trap ERR, exit codes, debugging techniques, and defensive scripting

📋 Table of Contents

Overview

Proper error handling and debugging are essential for writing reliable Bash scripts. This includes using set options to control behavior, trapping errors, validating inputs, and using debugging tools to find and fix problems.

Key Concepts

Exit Codes: Every command returns 0 for success, non-zero for failure

Set Options: Control script behavior with set -e, set -u, etc.

Traps: Catch signals and errors to perform cleanup or logging

Debugging: Use set -x, PS4, and other tools to trace execution

Set Options Reference

OptionLong FormDescription
set -e set -o errexit Exit on first error
set -u set -o nounset Error on undefined variables
set -o pipefail - Pipeline fails if any command fails
set -x set -o xtrace Print commands before execution
set -v set -o verbose Print lines as read
set -n set -o noexec Check syntax without running
set -E set -o errtrace ERR trap inherited by functions
set -T set -o functrace DEBUG trap inherited by functions
💡 Recommended Script Header
set -euo pipefail is commonly called "strict mode" and catches most common errors.

Standard Exit Codes

CodeMeaningExample
0 Success Command completed successfully
1 General error Miscellaneous errors
2 Misuse of shell command Invalid arguments
126 Command not executable Permission denied
127 Command not found Binary doesn't exist in PATH
128+N Killed by signal N 130 = Ctrl+C (SIGINT)

Detailed Examples

1
Basic Error Checking

Checking command exit status and handling errors.

#!/bin/bash
# Basic error checking

echo "=== Check Exit Status ==="

# $? contains last exit status
ls /tmp > /dev/null
echo "ls /tmp: $?"

ls /nonexistent 2>/dev/null
echo "ls /nonexistent: $?"

echo ""
echo "=== If Statement Check ==="

if ls /tmp > /dev/null 2>&1; then
    echo "ls succeeded"
else
    echo "ls failed"
fi

if ls /nonexistent > /dev/null 2>&1; then
    echo "ls succeeded"
else
    echo "ls failed"
fi

echo ""
echo "=== Short-circuit Operators ==="

# && runs second command if first succeeds
ls /tmp > /dev/null && echo "Directory exists"

# || runs second command if first fails
ls /nonexistent 2>/dev/null || echo "Directory missing"

# Combined pattern
ls /nonexistent 2>/dev/null && echo "Found" || echo "Not found"

echo ""
echo "=== Check Specific Commands ==="

# Check if command exists
if command -v git > /dev/null 2>&1; then
    echo "git is installed"
else
    echo "git is not installed"
fi

# Check if file is readable
file="/etc/passwd"
if [[ -r "$file" ]]; then
    echo "$file is readable"
fi

echo ""
echo "=== Function Return Values ==="

validate_number() {
    local num=$1
    
    if [[ "$num" =~ ^[0-9]+$ ]]; then
        return 0  # Success
    else
        return 1  # Failure
    fi
}

if validate_number "42"; then
    echo "42 is valid"
fi

if ! validate_number "abc"; then
    echo "abc is not a number"
fi

echo ""
echo "=== Die Function Pattern ==="

die() {
    echo "ERROR: $1" >&2
    exit "${2:-1}"
}

# Usage examples (commented to not exit script)
# [[ -f "$config" ]] || die "Config not found: $config"
# command -v docker &>/dev/null || die "Docker required" 2

echo "die() function defined for error handling"

echo ""
echo "=== Multiple Conditions ==="

check_prerequisites() {
    local errors=0
    
    if [[ ! -d "/tmp" ]]; then
        echo "ERROR: /tmp missing" >&2
        ((errors++))
    fi
    
    if ! command -v bash &>/dev/null; then
        echo "ERROR: bash required" >&2
        ((errors++))
    fi
    
    return $errors
}

if check_prerequisites; then
    echo "All prerequisites met"
else
    echo "Prerequisites check failed"
fi
Output: === Check Exit Status === ls /tmp: 0 ls /nonexistent: 2 === If Statement Check === ls succeeded ls failed === Short-circuit Operators === Directory exists Directory missing Not found === Check Specific Commands === git is installed /etc/passwd is readable === Function Return Values === 42 is valid abc is not a number === Multiple Conditions === All prerequisites met
2
Strict Mode (set -euo pipefail)

Using set options for automatic error detection.

#!/bin/bash
# Strict mode demonstration

echo "=== set -e (errexit) ==="

# Exit on first error
(
    set -e
    echo "Before error"
    false  # This causes exit
    echo "After error (never reached)"
) || echo "Subshell exited with error"

echo ""
echo "=== set -u (nounset) ==="

# Error on undefined variables
(
    set -u
    echo "Defined: ${HOME}"
    # echo "Undefined: ${UNDEFINED_VAR}"  # Would cause error
) || echo "Subshell exited"

# Safe way to use undefined variables
(
    set -u
    echo "With default: ${UNDEFINED:-default_value}"
    echo "Empty if unset: ${ALSO_UNDEFINED:-}"
)

echo ""
echo "=== set -o pipefail ==="

# Without pipefail
false | true
echo "Without pipefail: $?"  # 0 (last command)

# With pipefail
(
    set -o pipefail
    false | true
    echo "Shouldn't reach here"
) || echo "Pipeline failed with pipefail"

echo ""
echo "=== Combined Strict Mode ==="

strict_function() {
    set -euo pipefail
    
    echo "Running in strict mode"
    
    # These would all cause immediate exit:
    # - false
    # - undefined_var
    # - false | true
    
    echo "Strict mode passed"
}

strict_function

echo ""
echo "=== PIPESTATUS Array ==="

set -o pipefail
false | true | false
echo "Exit status: $?"
# Can't use PIPESTATUS after set -o pipefail exits

# Save PIPESTATUS immediately
cat /nonexistent 2>/dev/null | grep "x" | wc -l
statuses=("${PIPESTATUS[@]}")
echo "Statuses: ${statuses[*]}"

echo ""
echo "=== Handling Expected Failures ==="

set -e

# Method 1: || true
rm /nonexistent 2>/dev/null || true
echo "Continued after expected failure"

# Method 2: Explicit handling
if ! rm /another_nonexistent 2>/dev/null; then
    echo "File didn't exist (expected)"
fi

# Method 3: Temporarily disable
set +e
rm /yet_another 2>/dev/null
exit_code=$?
set -e
echo "Captured exit code: $exit_code"

echo ""
echo "=== Full Script Header ==="

cat << 'EOF'
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

# Script content here...
EOF

echo "(Example script header shown above)"
Output: === set -e (errexit) === Before error Subshell exited with error === set -u (nounset) === Defined: /home/user With default: default_value Empty if unset: === set -o pipefail === Without pipefail: 0 Pipeline failed with pipefail === Combined Strict Mode === Running in strict mode Strict mode passed === PIPESTATUS Array === Exit status: 1 Statuses: 1 1 0 === Handling Expected Failures === Continued after expected failure File didn't exist (expected) Captured exit code: 1 === Full Script Header === #!/bin/bash set -euo pipefail IFS=$'\n\t' # Script content here...
⚠️ set -e Gotchas
set -e doesn't trigger in: if conditions, commands before || or &&, commands in pipelines (unless pipefail), or negated commands (! cmd).
3
Error Traps

Using trap to catch errors and perform cleanup.

#!/bin/bash
# Error traps

echo "=== Basic ERR Trap ==="

err_handler() {
    echo "ERROR on line $1: command '$2' failed with exit code $3"
}

trap 'err_handler $LINENO "$BASH_COMMAND" $?' ERR

# This triggers the trap
(
    false
) 2>/dev/null

# Reset trap
trap - ERR

echo ""
echo "=== EXIT Trap for Cleanup ==="

cleanup() {
    local exit_code=$?
    echo "Cleanup: exit code was $exit_code"
    rm -f /tmp/test_$$_*
    # Return original exit code
    exit $exit_code
}

trap cleanup EXIT

# Create temp files
touch /tmp/test_$$_file1
touch /tmp/test_$$_file2
echo "Created temp files"

# Cleanup runs on exit (even from set -e)

echo ""
echo "=== Trap with Line Numbers ==="

set -E  # Inherit ERR trap in functions

error_report() {
    echo "Error at:"
    echo "  File: ${BASH_SOURCE[1]:-unknown}"
    echo "  Line: ${BASH_LINENO[0]:-unknown}"
    echo "  Func: ${FUNCNAME[1]:-main}"
    echo "  Command: $BASH_COMMAND"
}

trap error_report ERR

# Test function
risky_function() {
    echo "In risky_function"
    false  # This triggers trap
}

(risky_function) 2>/dev/null || true

trap - ERR
set +E

echo ""
echo "=== Multiple Traps ==="

trap 'echo "Caught INT"' INT
trap 'echo "Caught TERM"' TERM
trap 'echo "Exiting"' EXIT

echo "Traps set"

# Show current traps
echo ""
echo "Current traps:"
trap -p

trap - INT TERM EXIT

echo ""
echo "=== Trap Stack Pattern ==="

# Save and restore traps
push_trap() {
    local new_trap=$1
    local signal=$2
    local old_trap=$(trap -p $signal)
    
    trap "$new_trap; $old_trap" $signal
}

trap 'echo "Original EXIT"' EXIT
push_trap 'echo "Added EXIT"' EXIT

echo "Testing trap stack (triggers on exit)"

echo ""
echo "=== Practical: Transaction Pattern ==="

transaction() {
    local rollback_commands=()
    
    rollback() {
        echo "Rolling back..."
        for ((i=${#rollback_commands[@]}-1; i>=0; i--)); do
            eval "${rollback_commands[$i]}"
        done
    }
    
    trap rollback ERR
    
    # Step 1
    mkdir -p /tmp/trans_test
    rollback_commands+=("rmdir /tmp/trans_test")
    echo "Step 1: created directory"
    
    # Step 2
    touch /tmp/trans_test/file
    rollback_commands+=("rm /tmp/trans_test/file")
    echo "Step 2: created file"
    
    # Simulate success
    echo "Transaction complete"
    
    trap - ERR
    
    # Cleanup
    rm -rf /tmp/trans_test
}

transaction
Output: === Basic ERR Trap === ERROR on line 12: command 'false' failed with exit code 1 === EXIT Trap for Cleanup === Created temp files Cleanup: exit code was 0 === Trap with Line Numbers === In risky_function Error at: File: script.sh Line: 45 Func: risky_function Command: false === Multiple Traps === Traps set Current traps: trap -- 'echo "Caught INT"' SIGINT trap -- 'echo "Caught TERM"' SIGTERM trap -- 'echo "Exiting"' EXIT === Practical: Transaction Pattern === Step 1: created directory Step 2: created file Transaction complete
4
Debugging with set -x

Tracing script execution for debugging.

#!/bin/bash
# Debugging with set -x

echo "=== Basic Trace ==="

set -x
name="World"
greeting="Hello, $name"
echo "$greeting"
set +x

echo ""
echo "=== Custom PS4 Prompt ==="

# PS4 controls trace output format
export PS4='+ ${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'

set -x
result=$((5 + 3))
set +x

echo "Result: $result"

# Reset PS4
PS4='+ '

echo ""
echo "=== Trace Specific Section ==="

process_data() {
    local input=$1
    
    # Only trace this section
    set -x
    local processed=$(echo "$input" | tr 'a-z' 'A-Z')
    set +x
    
    echo "$processed"
}

output=$(process_data "hello world")
echo "Output: $output"

echo ""
echo "=== Debug to File ==="

# Redirect trace to file
exec 2>/tmp/debug.log

set -x
for i in 1 2 3; do
    echo "Item $i"
done > /dev/null
set +x

exec 2>&1

echo "Debug output saved to /tmp/debug.log"
head -5 /tmp/debug.log
rm /tmp/debug.log

echo ""
echo "=== Conditional Debug ==="

DEBUG=${DEBUG:-false}

debug() {
    if $DEBUG; then
        echo "DEBUG: $*" >&2
    fi
}

debug "This is hidden by default"

DEBUG=true
debug "This would show"
DEBUG=false

echo ""
echo "=== BASH_XTRACEFD ==="

# Redirect trace to specific fd
exec 3>/tmp/trace.log
BASH_XTRACEFD=3

set -x
echo "Traced to fd 3" > /dev/null
set +x

exec 3>&-
unset BASH_XTRACEFD

echo "Trace in /tmp/trace.log:"
cat /tmp/trace.log
rm /tmp/trace.log

echo ""
echo "=== Debug Function Calls ==="

func_a() {
    echo "In func_a"
    func_b
}

func_b() {
    echo "In func_b"
    echo "Call stack: ${FUNCNAME[*]}"
    echo "Line numbers: ${BASH_LINENO[*]}"
}

func_a

echo ""
echo "=== set -v vs set -x ==="

echo "set -v shows lines as read:"
set -v
name="test"
set +v

echo ""
echo "set -x shows expanded commands:"
set -x
name="test"
set +x
Output: === Basic Trace === + name=World + greeting='Hello, World' + echo 'Hello, World' Hello, World + set +x === Custom PS4 Prompt === + script.sh:20: result=8 + script.sh:21: set +x Result: 8 === Trace Specific Section === + local processed=HELLO WORLD + set +x Output: HELLO WORLD === Debug to File === Debug output saved to /tmp/debug.log + for i in 1 2 3 + echo 'Item 1' + for i in 1 2 3 + echo 'Item 2' === Debug Function Calls === In func_a In func_b Call stack: func_b func_a main Line numbers: 73 69 0 === set -v vs set -x === set -v shows lines as read: name="test" set -x shows expanded commands: + name=test
5
Input Validation

Validating arguments and user input.

#!/bin/bash
# Input validation

echo "=== Check Required Arguments ==="

demo_func() {
    if [[ $# -lt 2 ]]; then
        echo "Usage: demo_func <arg1> <arg2>" >&2
        return 1
    fi
    
    local arg1=$1
    local arg2=$2
    
    echo "Got: $arg1, $arg2"
}

demo_func "one" || true
demo_func "one" "two"

echo ""
echo "=== Validate Argument Types ==="

is_number() {
    [[ "$1" =~ ^-?[0-9]+$ ]]
}

is_positive() {
    [[ "$1" =~ ^[0-9]+$ ]] && [[ "$1" -gt 0 ]]
}

is_ip_address() {
    local ip=$1
    local IFS='.'
    local -a octets=($ip)
    
    [[ ${#octets[@]} -eq 4 ]] || return 1
    
    for octet in "${octets[@]}"; do
        [[ "$octet" =~ ^[0-9]+$ ]] || return 1
        ((octet >= 0 && octet <= 255)) || return 1
    done
}

# Tests
for val in "42" "-5" "abc" "3.14"; do
    is_number "$val" && echo "$val: number" || echo "$val: not number"
done

echo ""
for ip in "192.168.1.1" "256.1.1.1" "not.an.ip"; do
    is_ip_address "$ip" && echo "$ip: valid" || echo "$ip: invalid"
done

echo ""
echo "=== Validate File Arguments ==="

check_input_file() {
    local file=$1
    
    if [[ -z "$file" ]]; then
        echo "Error: no file specified" >&2
        return 1
    fi
    
    if [[ ! -e "$file" ]]; then
        echo "Error: file not found: $file" >&2
        return 1
    fi
    
    if [[ ! -f "$file" ]]; then
        echo "Error: not a regular file: $file" >&2
        return 1
    fi
    
    if [[ ! -r "$file" ]]; then
        echo "Error: file not readable: $file" >&2
        return 1
    fi
    
    return 0
}

check_input_file "/etc/passwd" && echo "/etc/passwd: OK"
check_input_file "/nonexistent" || true
check_input_file "/tmp" || true

echo ""
echo "=== getopts for Option Parsing ==="

parse_options() {
    local verbose=false
    local output=""
    local count=1
    
    OPTIND=1
    while getopts "vo:n:" opt; do
        case $opt in
            v) verbose=true ;;
            o) output="$OPTARG" ;;
            n) 
                if ! is_positive "$OPTARG"; then
                    echo "Error: -n requires positive number" >&2
                    return 1
                fi
                count="$OPTARG"
                ;;
            ?) return 1 ;;
        esac
    done
    
    shift $((OPTIND - 1))
    
    echo "verbose=$verbose output='$output' count=$count"
    echo "Remaining args: $*"
}

parse_options -v -o out.txt -n 5 file1 file2

echo ""
echo "=== Sanitize Input ==="

sanitize_filename() {
    local name=$1
    
    # Remove dangerous characters
    name="${name//[^a-zA-Z0-9._-]/}"
    
    # Prevent directory traversal
    name="${name#/}"
    name="${name#../}"
    
    echo "$name"
}

for input in "normal.txt" "../etc/passwd" "file; rm -rf /" "good-file_1.txt"; do
    echo "$input -> $(sanitize_filename "$input")"
done
Output: === Check Required Arguments === Usage: demo_func <arg1> <arg2> Got: one, two === Validate Argument Types === 42: number -5: number abc: not number 3.14: not number 192.168.1.1: valid 256.1.1.1: invalid not.an.ip: invalid === Validate File Arguments === /etc/passwd: OK Error: file not found: /nonexistent Error: not a regular file: /tmp === getopts for Option Parsing === verbose=false output='out.txt' count=5 Remaining args: file1 file2 === Sanitize Input === normal.txt -> normal.txt ../etc/passwd -> etcpasswd file; rm -rf / -> filerm-rf good-file_1.txt -> good-file_1.txt
6
Logging and Error Messages

Structured logging and informative error messages.

#!/bin/bash
# Logging and error messages

echo "=== Basic Logging Functions ==="

# Log levels
LOG_ERROR=0
LOG_WARN=1
LOG_INFO=2
LOG_DEBUG=3

LOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}

log() {
    local level=$1
    local msg=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    if [[ $level -le $LOG_LEVEL ]]; then
        echo "[$timestamp] $msg" >&2
    fi
}

log_error() { log $LOG_ERROR "[ERROR] $1"; }
log_warn()  { log $LOG_WARN  "[WARN]  $1"; }
log_info()  { log $LOG_INFO  "[INFO]  $1"; }
log_debug() { log $LOG_DEBUG "[DEBUG] $1"; }

log_info "Application started"
log_debug "Debug info (hidden at INFO level)"
log_warn "Something to watch"
log_error "Something went wrong"

echo ""
echo "=== Colored Output ==="

# Colors
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'  # No Color

print_error() { echo -e "${RED}ERROR: $1${NC}" >&2; }
print_warn()  { echo -e "${YELLOW}WARNING: $1${NC}" >&2; }
print_info()  { echo -e "${GREEN}INFO: $1${NC}"; }
print_debug() { echo -e "${BLUE}DEBUG: $1${NC}"; }

print_info "Operation successful"
print_warn "Deprecated feature"
print_error "Connection failed"

echo ""
echo "=== Context in Error Messages ==="

error_with_context() {
    local msg=$1
    local file=${BASH_SOURCE[1]:-unknown}
    local line=${BASH_LINENO[0]:-?}
    local func=${FUNCNAME[1]:-main}
    
    echo "ERROR in $func() at $file:$line: $msg" >&2
}

failing_function() {
    error_with_context "Something went wrong"
}

failing_function

echo ""
echo "=== Log to File ==="

LOGFILE="/tmp/script.log"

init_logging() {
    exec 3>> "$LOGFILE"
    log_info "Logging initialized to $LOGFILE"
}

close_logging() {
    exec 3>&-
}

file_log() {
    echo "[$(date '+%H:%M:%S')] $1" >&3
}

init_logging
file_log "This goes to log file"
file_log "Another message"
close_logging

echo "Log file contents:"
cat "$LOGFILE"
rm "$LOGFILE"

echo ""
echo "=== Progress Reporting ==="

progress() {
    local current=$1
    local total=$2
    local width=40
    local pct=$((current * 100 / total))
    local filled=$((current * width / total))
    local empty=$((width - filled))
    
    printf "\r["
    printf "%${filled}s" | tr ' ' '#'
    printf "%${empty}s" | tr ' ' '-'
    printf "] %3d%%" $pct
}

echo "Progress bar:"
for i in {1..10}; do
    progress $i 10
    sleep 0.1
done
echo ""

echo ""
echo "=== Structured Error Info ==="

create_error() {
    local code=$1
    local msg=$2
    local details=$3
    
    cat << EOF
{
  "error": {
    "code": $code,
    "message": "$msg",
    "details": "$details",
    "timestamp": "$(date -Iseconds)"
  }
}
EOF
}

create_error 404 "File not found" "/path/to/missing"
Output: === Basic Logging Functions === [2025-11-20 10:30:45] [INFO] Application started [2025-11-20 10:30:45] [WARN] Something to watch [2025-11-20 10:30:45] [ERROR] Something went wrong === Colored Output === INFO: Operation successful WARNING: Deprecated feature ERROR: Connection failed === Context in Error Messages === ERROR in failing_function() at script.sh:75: Something went wrong === Log to File === Log file contents: [10:30:45] This goes to log file [10:30:45] Another message === Progress Reporting === Progress bar: [########################################] 100% === Structured Error Info === { "error": { "code": 404, "message": "File not found", "details": "/path/to/missing", "timestamp": "2025-11-20T10:30:45+00:00" } }
7
Assertions and Preconditions

Validating assumptions with assertion functions.

#!/bin/bash
# Assertions and preconditions

echo "=== Basic Assert ==="

assert() {
    local condition=$1
    local message=${2:-"Assertion failed"}
    
    if ! eval "$condition"; then
        echo "ASSERT FAILED: $message" >&2
        echo "  Condition: $condition" >&2
        echo "  Location: ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" >&2
        return 1
    fi
}

# Tests
assert '[[ 1 -eq 1 ]]' "1 should equal 1"
echo "Assertion passed"

assert '[[ -d /tmp ]]' "/tmp should exist"
echo "Directory assertion passed"

# This would fail:
# assert '[[ 1 -eq 2 ]]' "1 should equal 2"

echo ""
echo "=== Typed Assertions ==="

assert_equals() {
    local expected=$1
    local actual=$2
    local msg=${3:-"Values should be equal"}
    
    if [[ "$expected" != "$actual" ]]; then
        echo "ASSERT FAILED: $msg" >&2
        echo "  Expected: $expected" >&2
        echo "  Actual:   $actual" >&2
        return 1
    fi
}

assert_not_empty() {
    local value=$1
    local msg=${2:-"Value should not be empty"}
    
    if [[ -z "$value" ]]; then
        echo "ASSERT FAILED: $msg" >&2
        return 1
    fi
}

assert_file_exists() {
    local file=$1
    
    if [[ ! -f "$file" ]]; then
        echo "ASSERT FAILED: File should exist: $file" >&2
        return 1
    fi
}

# Tests
assert_equals "hello" "hello" "Strings match"
assert_not_empty "value" "Has content"
assert_file_exists "/etc/passwd"
echo "All typed assertions passed"

echo ""
echo "=== Preconditions ==="

require_root() {
    if [[ $EUID -ne 0 ]]; then
        echo "ERROR: This script must be run as root" >&2
        return 1
    fi
}

require_command() {
    local cmd=$1
    
    if ! command -v "$cmd" &>/dev/null; then
        echo "ERROR: Required command not found: $cmd" >&2
        return 1
    fi
}

require_var() {
    local var_name=$1
    local var_value="${!var_name}"
    
    if [[ -z "$var_value" ]]; then
        echo "ERROR: Required variable not set: $var_name" >&2
        return 1
    fi
}

# Tests
require_command "bash" && echo "bash found"
require_command "ls" && echo "ls found"

MY_VAR="set"
require_var "MY_VAR" && echo "MY_VAR is set"

echo ""
echo "=== Contract Functions ==="

# Require preconditions
divide() {
    local numerator=$1
    local denominator=$2
    
    # Preconditions
    assert_not_empty "$numerator" "Numerator required"
    assert_not_empty "$denominator" "Denominator required"
    assert '[[ "$denominator" != "0" ]]' "Cannot divide by zero"
    
    echo $((numerator / denominator))
}

result=$(divide 10 2)
echo "10 / 2 = $result"

# This would fail:
# divide 10 0

echo ""
echo "=== Test Mode ==="

run_tests() {
    local failures=0
    
    # Test 1
    if assert_equals "abc" "abc" "Test 1: string compare"; then
        echo "✓ Test 1 passed"
    else
        ((failures++))
    fi
    
    # Test 2
    if assert '[[ 5 -gt 3 ]]' "Test 2: numeric compare"; then
        echo "✓ Test 2 passed"
    else
        ((failures++))
    fi
    
    # Test 3
    if assert_file_exists "/etc/passwd"; then
        echo "✓ Test 3 passed"
    else
        ((failures++))
    fi
    
    echo ""
    if [[ $failures -eq 0 ]]; then
        echo "All tests passed!"
    else
        echo "$failures test(s) failed"
    fi
    
    return $failures
}

run_tests
Output: === Basic Assert === Assertion passed Directory assertion passed === Typed Assertions === All typed assertions passed === Preconditions === bash found ls found MY_VAR is set === Contract Functions === 10 / 2 = 5 === Test Mode === ✓ Test 1 passed ✓ Test 2 passed ✓ Test 3 passed All tests passed!
8
Static Analysis with shellcheck

Using shellcheck and other tools for code quality.

#!/bin/bash
# Static analysis and code quality

echo "=== Common Issues shellcheck Catches ==="

cat << 'EOF'
# Issue 1: Unquoted variables
file=$1
cat $file      # SC2086: Double quote to prevent globbing

# Issue 2: Useless use of cat
cat file | grep pattern   # SC2002: Useless cat

# Issue 3: == in [ ]
if [ "$a" == "$b" ]       # SC2039: == not POSIX, use =

# Issue 4: Unused variables
unused="value"             # SC2034: Unused variable

# Issue 5: Command in [ ]
if [ grep -q "x" file ]   # SC2046: Use $() for command
EOF

echo ""
echo "=== Running shellcheck ==="

# Check if shellcheck is installed
if command -v shellcheck &>/dev/null; then
    # Create test script
    cat > /tmp/test_script.sh << 'EOF'
#!/bin/bash
file=$1
cat $file
EOF
    
    echo "Checking /tmp/test_script.sh:"
    shellcheck /tmp/test_script.sh || true
    rm /tmp/test_script.sh
else
    echo "shellcheck not installed"
    echo "Install with: apt install shellcheck"
fi

echo ""
echo "=== Syntax Check with bash -n ==="

# Create scripts
cat > /tmp/good.sh << 'EOF'
#!/bin/bash
echo "Hello"
if [[ -f /tmp ]]; then
    echo "exists"
fi
EOF

cat > /tmp/bad.sh << 'EOF'
#!/bin/bash
echo "Hello"
if [[ -f /tmp ]]; then
    echo "missing fi"
EOF

echo "Checking good.sh:"
if bash -n /tmp/good.sh; then
    echo "  Syntax OK"
fi

echo ""
echo "Checking bad.sh:"
bash -n /tmp/bad.sh 2>&1 || true

rm /tmp/good.sh /tmp/bad.sh

echo ""
echo "=== Best Practices ==="

cat << 'EOF'
# 1. Always quote variables
echo "$variable"

# 2. Use [[ ]] instead of [ ]
if [[ "$a" == "$b" ]]; then

# 3. Use $() instead of backticks
result=$(command)

# 4. Use arrays for lists
files=(*.txt)
for f in "${files[@]}"; do

# 5. Check command existence
if command -v git &>/dev/null; then

# 6. Use local in functions
myfunc() {
    local var="value"
}

# 7. Always use set -euo pipefail
set -euo pipefail
EOF

echo ""
echo "=== Defensive Programming ==="

cat << 'EOF'
# Default values
: "${CONFIG:=/etc/default.conf}"

# Readonly for constants
readonly MAX_RETRIES=3

# Validate early
[[ -f "$input" ]] || die "Input not found"

# Use arrays properly
files=("file with spaces.txt")
for f in "${files[@]}"; do  # Not $files or ${files[*]}
EOF

echo ""
echo "=== Portable Code Tips ==="

echo "For POSIX compatibility:"
echo "  - Use [ ] not [[ ]]"
echo "  - Use \$(cmd) not \$((expr))"
echo "  - Use = not == for string compare"
echo "  - Avoid arrays and associative arrays"
Output: === Common Issues shellcheck Catches === # Issue 1: Unquoted variables file=$1 cat $file # SC2086: Double quote to prevent globbing ... === Running shellcheck === Checking /tmp/test_script.sh: In /tmp/test_script.sh line 3: cat $file ^---^ SC2086: Double quote to prevent globbing and word splitting. === Syntax Check with bash -n === Checking good.sh: Syntax OK Checking bad.sh: /tmp/bad.sh: line 5: syntax error: unexpected end of file === Best Practices === # 1. Always quote variables echo "$variable" ...
💡 Install shellcheck
shellcheck is essential for Bash development. Install with apt install shellcheck or brew install shellcheck.
9
Retry and Resilience Patterns

Making scripts resilient to transient failures.

#!/bin/bash
# Retry and resilience patterns

echo "=== Basic Retry ==="

retry() {
    local max_attempts=$1
    local delay=$2
    shift 2
    
    local attempt=1
    
    while [[ $attempt -le $max_attempts ]]; do
        echo "Attempt $attempt of $max_attempts..."
        
        if "$@"; then
            echo "Success on attempt $attempt"
            return 0
        fi
        
        if [[ $attempt -lt $max_attempts ]]; then
            echo "Failed, waiting ${delay}s..."
            sleep "$delay"
        fi
        
        ((attempt++))
    done
    
    echo "All $max_attempts attempts failed"
    return 1
}

# Test with command that might fail
flaky_command() {
    # Simulate 50% failure rate
    [[ $((RANDOM % 2)) -eq 0 ]]
}

retry 3 1 flaky_command || true

echo ""
echo "=== Exponential Backoff ==="

retry_backoff() {
    local max_attempts=$1
    shift
    
    local attempt=1
    local delay=1
    
    while [[ $attempt -le $max_attempts ]]; do
        echo "Attempt $attempt (delay: ${delay}s)..."
        
        if "$@"; then
            return 0
        fi
        
        if [[ $attempt -lt $max_attempts ]]; then
            sleep "$delay"
            delay=$((delay * 2))  # Exponential
        fi
        
        ((attempt++))
    done
    
    return 1
}

echo "Exponential backoff pattern defined"

echo ""
echo "=== Circuit Breaker Pattern ==="

declare -A CIRCUIT_STATE
declare -A CIRCUIT_FAILURES

circuit_call() {
    local name=$1
    shift
    
    local state=${CIRCUIT_STATE[$name]:-closed}
    local failures=${CIRCUIT_FAILURES[$name]:-0}
    local threshold=3
    
    if [[ "$state" == "open" ]]; then
        echo "Circuit $name is OPEN, failing fast"
        return 1
    fi
    
    if "$@"; then
        CIRCUIT_FAILURES[$name]=0
        return 0
    else
        ((CIRCUIT_FAILURES[$name]++))
        
        if [[ ${CIRCUIT_FAILURES[$name]} -ge $threshold ]]; then
            CIRCUIT_STATE[$name]="open"
            echo "Circuit $name OPENED after $threshold failures"
        fi
        
        return 1
    fi
}

# Test circuit breaker
for i in {1..5}; do
    circuit_call "api" false || true
done

echo ""
echo "=== Timeout Wrapper ==="

with_timeout() {
    local timeout=$1
    shift
    
    timeout "$timeout" "$@"
    local status=$?
    
    if [[ $status -eq 124 ]]; then
        echo "Command timed out after ${timeout}s" >&2
    fi
    
    return $status
}

with_timeout 2 sleep 1 && echo "Completed in time"
with_timeout 1 sleep 5 || echo "Timed out"

echo ""
echo "=== Fallback Pattern ==="

with_fallback() {
    local primary=$1
    local fallback=$2
    
    if eval "$primary"; then
        return 0
    fi
    
    echo "Primary failed, trying fallback..."
    eval "$fallback"
}

# Test fallback
with_fallback 'ls /nonexistent 2>/dev/null' 'echo "Using default"'

echo ""
echo "=== Health Check ==="

health_check() {
    local name=$1
    local check=$2
    
    if eval "$check"; then
        echo "✓ $name: healthy"
        return 0
    else
        echo "✗ $name: unhealthy"
        return 1
    fi
}

# Run health checks
errors=0
health_check "Disk space" '[[ $(df -P / | awk "NR==2 {print \$5}" | tr -d %) -lt 90 ]]' || ((errors++))
health_check "Memory" '[[ $(free | awk "/Mem:/ {print int(\$3/\$2*100)}") -lt 90 ]]' || ((errors++))
health_check "/tmp writable" '[[ -w /tmp ]]' || ((errors++))

echo ""
echo "Health checks: $errors failures"
Output: === Basic Retry === Attempt 1 of 3... Failed, waiting 1s... Attempt 2 of 3... Success on attempt 2 === Exponential Backoff === Exponential backoff pattern defined === Circuit Breaker Pattern === Attempt 1 Attempt 2 Attempt 3 Circuit api OPENED after 3 failures Circuit api is OPEN, failing fast Circuit api is OPEN, failing fast === Timeout Wrapper === Completed in time Command timed out after 1s Timed out === Fallback Pattern === Primary failed, trying fallback... Using default === Health Check === ✓ Disk space: healthy ✓ Memory: healthy ✓ /tmp writable: healthy Health checks: 0 failures
10
Complete Error Handling Library

A reusable library for error handling and debugging.

#!/bin/bash
# Complete error handling library

#######################################
# Configuration
#######################################

set -euo pipefail

readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

#######################################
# Logging
#######################################

declare -r LOG_ERROR=0 LOG_WARN=1 LOG_INFO=2 LOG_DEBUG=3
LOG_LEVEL=${LOG_LEVEL:-$LOG_INFO}

_log() {
    local level=$1 msg=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    [[ $level -le $LOG_LEVEL ]] && echo "[$timestamp] $msg" >&2
}

log_error() { _log $LOG_ERROR "[ERROR] $1"; }
log_warn()  { _log $LOG_WARN  "[WARN]  $1"; }
log_info()  { _log $LOG_INFO  "[INFO]  $1"; }
log_debug() { _log $LOG_DEBUG "[DEBUG] $1"; }

#######################################
# Error Handling
#######################################

die() {
    log_error "$1"
    exit "${2:-1}"
}

err_trap() {
    local exit_code=$?
    local line=$1
    local command=$2
    log_error "Command '$command' failed at line $line (exit $exit_code)"
}

trap 'err_trap $LINENO "$BASH_COMMAND"' ERR

#######################################
# Assertions
#######################################

assert() {
    local cond=$1 msg=${2:-"Assertion failed"}
    eval "$cond" || die "$msg"
}

require_command() {
    command -v "$1" &>/dev/null || die "Required: $1"
}

require_var() {
    [[ -n "${!1:-}" ]] || die "Required variable: $1"
}

#######################################
# Resilience
#######################################

retry() {
    local attempts=$1 delay=$2
    shift 2
    
    for ((i=1; i<=attempts; i++)); do
        "$@" && return 0
        [[ $i -lt $attempts ]] && sleep "$delay"
    done
    return 1
}

with_timeout() {
    local secs=$1
    shift
    timeout "$secs" "$@"
}

#######################################
# Cleanup
#######################################

declare -a CLEANUP_COMMANDS=()

cleanup_add() {
    CLEANUP_COMMANDS+=("$1")
}

cleanup_run() {
    local exit_code=$?
    for ((i=${#CLEANUP_COMMANDS[@]}-1; i>=0; i--)); do
        eval "${CLEANUP_COMMANDS[$i]}" || true
    done
    exit $exit_code
}

trap cleanup_run EXIT

#######################################
# Demo
#######################################

main() {
    log_info "Starting $SCRIPT_NAME"
    
    # Setup
    local tmpdir=$(mktemp -d)
    cleanup_add "rm -rf '$tmpdir'"
    log_debug "Created temp dir: $tmpdir"
    
    # Preconditions
    require_command bash
    require_command grep
    
    # Work
    log_info "Performing operations..."
    
    echo "test" > "$tmpdir/file.txt"
    assert '[[ -f "$tmpdir/file.txt" ]]' "File should exist"
    
    # Simulate retry
    local count=0
    retry 3 1 bash -c '((++count > 1))' && log_info "Retry succeeded"
    
    log_info "Operations complete"
    
    # Cleanup runs automatically on exit
}

echo "========================================"
echo "   Error Handling Library Demo"
echo "========================================"
echo ""

main

echo ""
echo "========================================"
Output: ======================================== Error Handling Library Demo ======================================== [2025-11-20 10:30:45] [INFO] Starting script.sh [2025-11-20 10:30:45] [INFO] Performing operations... [2025-11-20 10:30:45] [INFO] Retry succeeded [2025-11-20 10:30:45] [INFO] Operations complete ========================================

Quick Reference

Essential Patterns

# Strict mode header
set -euo pipefail

# Error trap
trap 'echo "Error at $LINENO"' ERR

# Cleanup trap  
trap cleanup EXIT

# Die function
die() { echo "ERROR: $1" >&2; exit 1; }

# Check command exists
command -v git &>/dev/null || die "git required"

# Check variable set
[[ -n "${VAR:-}" ]] || die "VAR required"

Common Patterns

TaskCode
Exit on errorset -e
Undefined var errorset -u
Pipeline failset -o pipefail
Debug traceset -x
Syntax checkbash -n script.sh
Static analysisshellcheck script.sh
Default value${VAR:-default}
Required value${VAR:?error msg}
✓ Best Practices
✗ Common Mistakes
← Back to BashSpecial Index ↑ Back to EXPANDED