G
GuideDevOps
Lesson 11 of 15

Error Handling & Exit Codes

Part of the Shell Scripting (Bash) tutorial series.

Exit Codes

Understanding Exit Codes

Every command returns an exit code (0 for success, non-zero for failure):

# Check exit code of last command
ls /tmp
echo "Exit code: $?"         # 0 if success
 
# Failed command
ls /nonexistent
echo "Exit code: $?"         # Non-zero if failed

Custom Exit Codes

if [ ! -f "$1" ]; then
    echo "Error: File not found"
    exit 1
fi
 
if [ ! -r "$1" ]; then
    echo "Error: File not readable"
    exit 2
fi
 
echo "File processed successfully"
exit 0

Check Command Success

# Check immediately after command
command
if [ $? -ne 0 ]; then
    echo "Error occurred"
    exit 1
fi
 
# Direct check
if ! command; then
    echo "Command failed"
    exit 1
fi
 
# Using || (OR)
command || exit 1

set Error Options

#!/bin/bash
 
# Exit on any error
set -e
 
# Also on undefined variables
set -u
 
# Fail on pipe errors
set -o pipefail
 
# Or combine
set -euo pipefail

Error Reporting

Error Function

error() {
    echo "ERROR: $*" >&2
    exit 1
}
 
if [ ! -f "config.txt" ]; then
    error "Config file not found"
fi

With Line Numbers

error() {
    local linenum=$1
    local msg=$2
    echo "ERROR at line $linenum: $msg" >&2
    exit 1
}
 
if [ $? -ne 0 ]; then
    error $LINENO "Previous command failed"
fi

trap - Cleanup on Exit

#!/bin/bash
 
# Create temp file
tmpfile=$(mktemp)
 
# Ensure cleanup on exit
trap "rm -f $tmpfile" EXIT
 
echo "Processing..."
 
# tmpfile automatically removed on exit

Multiple Traps

cleanup() {
    echo "Cleaning up..."
    rm -f temp_files/*
    pkill -f "background_process"
}
 
error_handler() {
    echo "Error on line $1"
    cleanup
    exit 1
}
 
trap "error_handler $LINENO" ERR
trap cleanup EXIT
 
echo "Starting script"

Debugging

Debug Mode

# Run with debugging
bash -x script.sh
 
# Set debug in script
set -x          # Enable
set +x          # Disable
 
# Or use shopt
shopt -s extdebug

Debug PS4 Prompt

# Show filename and line
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

Conditional Debugging

# Enable debug mode if DEBUG env var set
[ "$DEBUG" = "1" ] && set -x
 
# Then run: DEBUG=1 ./script.sh

Try-Catch Pattern

# Simulate try-catch
try() {
    [[ $- = *e* ]]; SAVED_OPT_E=$?
    set +e
}
 
catch() {
    export exception_code=$?
    (( SAVED_OPT_E )) && set +e
    return $exception_code
}
 
try
(
    risky_command
    another_command
)
catch || {
    echo "Error: Commands failed with code $exception_code"
}

Validation

# Validate arguments
if [ $# -lt 2 ]; then
    echo "Usage: $0 source destination" >&2
    exit 1
fi
 
if [ ! -f "$1" ]; then
    echo "Error: Source file not found: $1" >&2
    exit 1
fi
 
if [ -z "$2" ]; then
    echo "Error: Destination cannot be empty" >&2
    exit 1
fi

Recovery Strategies

# Retry logic
retry_command() {
    local max_attempts=3
    local count=0
    while [ $count -lt $max_attempts ]; do
        if "$@"; then
            return 0
        fi
        ((count++))
        echo "Attempt $count failed, retrying..."
        sleep 2
    done
    return 1
}
 
if ! retry_command curl https://example.com; then
    echo "Failed after $max_attempts attempts"
    exit 1
fi