Bash Basics #6: Error Handling and Debugging in Bash

Make your Bash scripts fault-tolerant and robust: signal handling with trap, Bash Strict Mode, ERR trap, custom PS4 debug prompts, and structured syslog logging.

Reading time: 16 min

Welcome to the sixth part of our technical series on Linux and Bash programming!

In the previous modules, we have learned how to structure scripts, work with variables, use control structures, modularize functions, and control I/O data streams.

In a perfect world, every command runs error-free. In reality of server operations, however, network connections drop, disks fill up, users press Ctrl + C in the middle of an update, or permissions are missing. A script that runs uncontrolled in such moments or leaves temporary files as junk endangers system stability.

In this lesson, you will learn how to protect your scripts against failures: We cover signal traps (trap) for automatic cleanup, the Bash Strict Mode, error catches with ERR trap, advanced debugging techniques with set -x and $PS4, and professional logging via Syslog.

Signal Handling & Cleanup Routines with trap

When a script creates temporary directories, starts background processes, or sets locks, these must be reliably cleaned up on termination – even if the script is aborted early due to an error or by the user pressing Ctrl + C (SIGINT).

The trap command catches operating system signals and executes a defined cleanup function (cleanup handler):


┌─────────────────────────────────────────────────────────────┐
│                 SIGNAL HANDLING WITH TRAP                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Script runs normally ─────────────────────────────┐        │
│         │                                          │        │
│  [ User presses Ctrl+C (SIGINT) ]                  │        │
│  or [ Script terminates normally (EXIT) ]          │        │
│  or [ Kill signal received (SIGTERM) ]             ▼        │
│         │                                                   │
│         ▼                                                   │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ TRAP HANDLER (cleanup function):                      │  │
│  │ - Removes /tmp/scratch.XXXXXX                         │  │
│  │ - Stops background processes                           │  │
│  │ - Releases lock files                                  │  │
│  └──────────────────────────┬────────────────────────────┘  │
│                             │                               │
│                             ▼                               │
│  [ Script terminates cleanly with no remaining residues ]   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

The Most Important Signals for trap:

Signal Name Trigger & Meaning
EXIT Pseudo-signal Fires always when the script is terminated (regularly or via exit)
INT (2) SIGINT Keyboard abort by the user via Ctrl + C
TERM (15) SIGTERM Default termination signal (e.g. by kill or systemd)
HUP (1) SIGHUP Terminal window closed or SSH connection dropped
ERR Bash signal Fires immediately when a command returns an error code ($\neq 0$)

Practical Example: Safely Working with Temporary Directories


#!/usr/bin/env bash
set -euo pipefail

# 1. Create secure temporary directory
TEMP_DIR=$(mktemp -d /tmp/backup_job.XXXXXX)

# 2. Define cleanup function
cleanup() {
    local exit_code=$?
    echo "Running cleanup tasks..."
    # Remove temporary directory completely
    rm -rf "$TEMP_DIR"
    echo "Temporary files deleted. Exit code: $exit_code"
    exit "$exit_code"
}

# 3. Register trap on EXIT, INT and TERM
trap cleanup EXIT INT TERM

# Main program
echo "Working in directory: $TEMP_DIR"
tar -czf "$TEMP_DIR/data.tar.gz" /etc/hosts
sleep 2

# Regardless of whether the script ends regularly or is aborted here:
# The 'cleanup' function is GUARANTEED to execute!

Precise Error Localization: ERR Trap

With set -e, Bash aborts immediately on errors. However, by default it does not tell you which line the error occurred on.

With an ERR trap and the Bash special variables $LINENO and $BASH_COMMAND, you build a precise crash reporter:


#!/usr/bin/env bash
set -euo pipefail

error_handler() {
    local exit_code=$?
    local error_line=$1
    local error_command=$2
    
    echo "=========================================" >&2
    echo "CRITICAL ERROR IN SCRIPT!" >&2
    echo "Error line    : $error_line" >&2
    echo "Command       : $error_command" >&2
    echo "Exit code     : $exit_code" >&2
    echo "=========================================" >&2
    exit "$exit_code"
}

# Register trap: Pass line number and command to handler
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR

echo "Step 1: Initialization..."
echo "Step 2: Attempting invalid command..."

# This command will fail:
ls /path/that_definitely_does_not_exist

echo "Step 3: Will never be reached."

Output on crash:


Step 1: Initialization...
Step 2: Attempting invalid command...
ls: cannot access '/path/that_definitely_does_not_exist': No such file or directory
=========================================
CRITICAL ERROR IN SCRIPT!
Error line    : 23
Command       : ls /path/that_definitely_does_not_exist
Exit code     : 2
=========================================

Debugging Techniques: Tracing the Error

When a script produces unexpected results, powerful analysis tools are at your disposal:

1. Trace Mode (-x) and Fine-tuning with $PS4

By default, set -x displays a simple + before each command. By adjusting the special variable $PS4, Bash displays file names, exact line numbers, and function names at each step:


#!/usr/bin/env bash
set -euo pipefail

# Extended trace prompt for professional debugging:
export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'

# Enable debugging selectively for a block:
set -x

a=10
b=20
sum=$(( a + b ))

set +x # Disable debugging again

echo "Calculation complete: $sum"

2. Syntax Check Without Execution (bash -n)

Checks scripts purely for syntactic correctness (e.g. unclosed quotes, forgotten done or fi keywords), without executing dangerous commands:


bash -n deploy_production.sh

3. Static Analysis with ShellCheck


# Check script for best practices and common pitfalls:
shellcheck deploy_production.sh

Structured Logging: File and Syslog Integration

Professional administration scripts do not output status messages unformatted with echo, but use uniform log levels (INFO, WARN, ERROR) and integrate with the Linux system journal (syslog):


#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="/tmp/app_maintenance.log"
APP_NAME="BackupService"

log_msg() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    # 1. Write to log file
    echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
    
    # 2. Send to system journal / syslog (visible via journalctl):
    logger -t "$APP_NAME" "[$level] $msg"
    
    # 3. Display colored in terminal
    case "$level" in
        INFO)  echo -e "\e[1;32m[INFO]\e[0m  $msg" ;;
        WARN)  echo -e "\e[1;33m[WARN]\e[0m  $msg" >&2 ;;
        ERROR) echo -e "\e[1;31m[ERROR]\e[0m $msg" >&2 ;;
    esac
}

log_msg "INFO" "Maintenance task started."
log_msg "WARN" "High CPU usage detected (88%)."
log_msg "ERROR" "Could not establish database connection!"

Messages sent with logger can be filtered system-wide with journalctl -t BackupService.

Exercises & Practice Check

Practice tasks for lesson #6:

  1. Task 1 (Signal Trap & Lockfile):

Write a script single_instance.sh that creates a lockfile /tmp/my_app.lock on startup and ensures via trap that this file is deleted on termination (whether regular or via Ctrl + C).

  1. Task 2 (Crash Handler with Alert):

Implement an ERR trap that stores the exact line number in /tmp/crash.log on error and displays a structured warning to the user.

  1. Task 3 (Custom PS4 Debugger):

Adjust $PS4 so that the current time in format [HH:MM:SS] is displayed before each command.

Sample Solution for Task 1:


#!/usr/bin/env bash
set -euo pipefail

LOCK_FILE="/tmp/single_instance.lock"

# Check if an instance is already running
if [[ -f "$LOCK_FILE" ]]; then
    echo "Error: Script already running (lockfile $LOCK_FILE exists)!" >&2
    exit 1
fi

# Create lockfile
touch "$LOCK_FILE"

# Guarantee cleanup
cleanup() {
    rm -f "$LOCK_FILE"
    echo "Lockfile released."
}
trap cleanup EXIT INT TERM

echo "Script running exclusively. Press Ctrl+C to test the trap..."
sleep 5
echo "Work completed successfully."

Command Reference (Cheatsheet)

Command / Syntax Function & Description
trap 'cleanup' EXIT INT TERM Executes cleanup on termination or signal interruption
trap 'error_func $LINENO' ERR Catches command errors and passes the error line number
set -euo pipefail Activates the full Bash Strict Mode
set -x / set +x Enables / disables the interactive execution trace
export PS4='+ [${LINENO}] ' Defines the format of the debugging trace output
bash -n script.sh Checks script for syntax errors without execution
logger -t TAG "text" Writes a log message directly to the Linux system journal
$BASH_COMMAND Contains the currently failed command in the ERR trap

Further Resources

Resource Description
GNU Bash Trap Builtin Reference Official GNU documentation on signal handling with trap
Bash Basics #5: Input and Output in Bash The previous part: Data streams, pipes, and here-docs
Linux Command Line Processor Guide Fundamental knowledge on shells, I/O streams, and pipes
chmod and File Permissions Linux permission concepts in detail

Conclusion

Robust scripts differ from simple hobby scripts through predictable error handling and clean resource management. Through deliberate use of signal traps (trap), the Bash Strict Mode, and detailed logging, you protect your servers from orphaned lockfiles, uncleaned data residue, and unnoticed errors in the background.

💡 Tip: For all scripts that create temporary files in /tmp or lockfiles, immediately add a trap cleanup EXIT INT TERM after creation. This prevents aborted cron jobs from filling the disk with leftover junk.

In the seventh and final part of our series, we combine our knowledge and focus on advanced professional techniques: 👉 Next up: Bash Basics #7: Advanced Bash Techniques

👉 Course overview: All lessons of the Bash basics course

Share & export

Export as Markdown