11.2. Options & Traps for Error Handling#

11.2.1. Common Pitfalls#

1. Forgetting that set -e doesn’t catch all errors

# These DON'T trigger set -e:
if some_command; then  # Error ignored in if condition
  echo "OK"
fi

some_command || true  # Error suppressed with ||
! some_command  # Error suppressed with negation

# Fix: Use explicit checks or avoid the pattern
if ! some_command; then
  echo "Command failed" >&2
  return 1
fi

2. Trap handlers that don’t preserve exit codes

# Bad: Overwrites exit code
trap 'echo "Error!"' ERR
command_that_fails
echo $?  # Prints exit code of echo, not the command!

# Good: Preserve the exit code
trap 'echo "Error! (code: $?)"' ERR

3. Not unsetting traps when they’re no longer needed

# Bad: Inner function's trap persists
outer() {
  trap 'echo "Outer trap"' EXIT
  inner
}

inner() {
  trap 'echo "Inner trap"' EXIT
  # When inner exits, both traps run!
}

# Better: Unset when done
inner() {
  trap 'echo "Inner trap"' EXIT
  # ... do work ...
  trap - EXIT  # Remove the trap
}

4. Using set -e with functions that return non-zero intentionally

# Problem: This function returns error intentionally for testing
check_status() {
  [[ "$1" == "ok" ]] && return 0 || return 1
}

set -e
check_status "bad"  # Script EXITS here!

# Fix: Use || to handle expected failures
set -e
check_status "bad" || echo "Status check failed"

11.2.2. Real-World Example: Production-Grade Script#

#!/bin/bash

# Production script with comprehensive error handling
set -euo pipefail

# Configuration
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_FILE="/var/log/myscript.log"
readonly LOCK_FILE="/var/run/myscript.lock"

# Error handler with logging
error_exit() {
  local line_number=$1
  local error_code=$2
  
  {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: Command failed at line $line_number"
    echo "Exit code: $error_code"
    echo "Command: $BASH_COMMAND"
  } | tee -a "$LOG_FILE" >&2
  
  exit "$error_code"
}

# Cleanup on exit
cleanup() {
  local exit_code=$?
  
  # Remove lock file
  rm -f "$LOCK_FILE"
  
  # Log completion
  if [[ $exit_code -eq 0 ]]; then
    echo "[$(date)] Script completed successfully" >> "$LOG_FILE"
  else
    echo "[$(date)] Script failed with code $exit_code" >> "$LOG_FILE"
  fi
  
  return $exit_code
}

# Install handlers
trap 'error_exit $LINENO $?' ERR
trap cleanup EXIT

# Prevent concurrent execution
if [[ -f "$LOCK_FILE" ]]; then
  echo "Script already running (PID: $(cat $LOCK_FILE))" >&2
  exit 1
fi
echo $$ > "$LOCK_FILE"

# Main work
echo "[$(date)] Starting main work" >> "$LOG_FILE"
do_work || return $?
echo "[$(date)] Work completed" >> "$LOG_FILE"

11.2.3. Practical Trap Examples#

11.2.3.1. Error Handler with Context#

#!/bin/bash
set -euo pipefail

handle_error() {
  local line_number=$1
  local error_code=$2
  
  echo "Error on line $line_number: Command exited with code $error_code" >&2
  
  # Log to syslog for production
  logger -t myscript "Error at line $line_number (code $error_code)"
  
  # Send alert email
  echo "Script failed at line $line_number" | \
    mail -s "Alert: myscript failed" admin@example.com
}

trap 'handle_error $LINENO $?' ERR

# Script continues...

11.2.3.2. Graceful Shutdown#

#!/bin/bash

# Start a background process
long_running_job &
job_pid=$!

handle_interrupt() {
  echo "Interrupted! Cleaning up..."
  kill $job_pid 2>/dev/null || true
  exit 130  # 128 + SIGINT(2)
}

trap handle_interrupt INT TERM

wait $job_pid
echo "Job completed successfully"

11.2.3.3. Debug Tracing#

#!/bin/bash
set -x  # Enable tracing

debug_trace() {
  echo "[DEBUG] Command: $BASH_COMMAND"
  echo "[DEBUG] Line: $LINENO"
}

trap debug_trace DEBUG

# All commands are printed before execution
ls -la
grep pattern file.txt

11.2.4. The trap Command for Cleanup#

trap runs code when a signal is received or script exits:

11.2.4.1. Basic trap Syntax#

trap 'cleanup_function' EXIT
trap 'handle_error' ERR
trap 'handle_interrupt' INT
trap 'handle_term' TERM
trap 'handle_debug' DEBUG

11.2.4.2. Common Signals to Trap#

Signal

When

Usage

EXIT

Script exits (any reason)

Cleanup resources

ERR

Error detected

Error logging

INT

Ctrl+C pressed

Graceful shutdown

TERM

Termination signal

Clean exit

DEBUG

After each command

Debugging/tracing

11.2.4.3. Cleanup Pattern#

#!/bin/bash

# Create temporary resources
temp_file=$(mktemp)
pid_file="/tmp/daemon.pid"

# Cleanup function
cleanup() {
  local exit_code=$?
  rm -f "$temp_file"
  rm -f "$pid_file"
  echo "Cleaned up at $(date)"
  exit $exit_code
}

trap cleanup EXIT

# Script continues...
echo "Working..." > "$temp_file"

11.2.5. The set Command for Error Control#

The set command modifies shell behavior to catch errors early:

11.2.5.1. Key set Options#

Option

Short

Effect

set -e or set -o errexit

Exit on any error

set -u or set -o nounset

Error on undefined variables

set -x or set -o xtrace

Print commands before execution (debugging)

set -o pipefail

Pipeline fails if any command fails

set -o noclobber

Prevent file overwriting with >

set -C

Same as noclobber

11.2.5.2. Basic set -e Usage#

#!/bin/bash
set -e  # Exit on any error

echo "Step 1"
ls /nonexistent  # Script EXITS here
echo "Step 2"  # Never executes

11.2.5.3. set -u to Catch Typos#

#!/bin/bash
set -u  # Error on undefined variables

name="Alice"
echo $name   # OK
echo $Name   # ERROR: unbound variable

11.2.5.4. Combining Options#

#!/bin/bash
set -euo pipefail  # Best practice: strict mode

# Now:
# - Script exits on any error
# - Script errors on undefined variables
# - Pipeline fails if any command fails