Bash Error Handling and Debugging

A script that runs without error handling is fragile. If one command fails silently, the next command may run on bad data and produce unexpected results. Proper error handling makes scripts reliable, predictable, and easy to troubleshoot.

Exit Status Codes

Every command in Bash returns an exit status (also called exit code) when it finishes. The special variable $? stores this code.

┌──────────────────────────────────────────────────┐
│              Exit Status Meaning                 │
│                                                  │
│   0     → Command succeeded                      │
│   1     → General error                          │
│   2     → Misuse of shell command                │
│   126   → Command found but not executable       │
│   127   → Command not found                      │
│   128+  → Command terminated by a signal         │
└──────────────────────────────────────────────────┘
#!/bin/bash
ls /existing_folder
echo "Exit code: $?"    # prints 0

ls /nonexistent_folder
echo "Exit code: $?"    # prints 2 or similar non-zero

Checking Exit Status After a Command

#!/bin/bash
cp source.txt destination.txt
if [ $? -ne 0 ]; then
  echo "ERROR: Copy failed."
  exit 1
fi
echo "Copy successful."

set -e – Exit on Error

Adding set -e at the top of a script tells Bash to stop immediately if any command fails. Without this, Bash continues running even after an error.

#!/bin/bash
set -e

echo "Step 1: Starting"
cp nonexistent.txt /tmp/     # This will fail
echo "Step 2: This will NOT run because -e stops the script"

set -u – Exit on Undefined Variable

set -u causes Bash to exit with an error if an undefined variable is used. This catches typos in variable names.

#!/bin/bash
set -u

username="Alice"
echo "Hello, $usernme"   # Typo: usernme instead of username
# Script stops with error: unbound variable

set -o pipefail – Catch Errors in Pipes

Normally, the exit status of a pipeline is the exit status of the last command only. With set -o pipefail, the pipeline fails if any command in it fails.

#!/bin/bash
set -o pipefail

cat nonexistent.txt | wc -l   # cat fails, pipefail catches it
echo "Exit: $?"

Using All Three Together (Best Practice)

#!/bin/bash
set -euo pipefail

This single line activates all three safety options and is the recommended starting point for any production Bash script.

trap – Run Cleanup on Exit or Error

The trap command runs a specified command when the script exits or encounters a specific signal. This is ideal for cleanup tasks like deleting temporary files.

Cleanup on Exit

#!/bin/bash
tmpfile=$(mktemp)

trap "rm -f $tmpfile; echo 'Cleaned up temp file.'" EXIT

echo "Working with $tmpfile"
echo "Some data" > $tmpfile
cat $tmpfile

# When script exits (normally or due to error), trap runs cleanup

Trap Signals

SignalMeaning
EXITScript finishes (any reason)
ERRAny command returns non-zero exit code
INTUser presses Ctrl+C
TERMScript receives a kill signal

Trap on ERR

#!/bin/bash
set -e
trap 'echo "ERROR occurred on line $LINENO"' ERR

echo "Step 1"
ls /nonexistent   # triggers ERR trap
echo "Step 2"    # not reached

Output:

Step 1
ERROR occurred on line 5

Custom Error Messages with a Function

#!/bin/bash
error_exit() {
  echo "ERROR: $1" >&2   # Print to stderr
  exit 1
}

if [ ! -f "config.txt" ]; then
  error_exit "config.txt not found. Cannot continue."
fi

Debugging with set -x

set -x enables debug mode. Bash prints each command before executing it, prefixed with +. This helps trace exactly what the script is doing.

#!/bin/bash
set -x

name="Alice"
echo "Hello, $name"

Output:

+ name=Alice
+ echo 'Hello, Alice'
Hello, Alice

Enable Debugging for Part of the Script Only

#!/bin/bash
echo "Normal mode"

set -x
# Debug this block only
name="Bob"
echo "Debugging: $name"
set +x

echo "Back to normal mode"

Debugging Flow Diagram

┌──────────────────────────────────────────────────────┐
│               Debugging Tools Summary                │
│                                                      │
│  set -e        → Stop on first error                 │
│  set -u        → Stop on undefined variable          │
│  set -x        → Print each command before running   │
│  set -o pipefail→ Catch errors inside pipes          │
│  trap ERR      → Run cleanup code on any error       │
│  $?            → Check last command exit code        │
│  echo to stderr→ Send error messages to stderr       │
└──────────────────────────────────────────────────────┘

Logging Errors to a File

#!/bin/bash
LOGFILE="script.log"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOGFILE
}

log "Script started"

if ! ping -c 1 google.com &>/dev/null; then
  log "ERROR: No network connection"
  exit 1
fi

log "Network check passed"
log "Script completed"

bash -n – Check Script Syntax Without Running

bash -n myscript.sh

This checks the script for syntax errors without actually running any commands. A good practice before running any new script.

Key Takeaways

  • Always check $? after important commands to detect failures.
  • Use set -euo pipefail at the top of every script for robust error handling.
  • Use trap to run cleanup code when the script exits or errors occur.
  • Use set -x to print each command during execution for debugging.
  • Use bash -n script.sh to check syntax errors before running.
  • Send error messages to stderr using echo "error" >&2 so they appear separately from normal output.

Leave a Comment