---
id: bash-basics-6-error-handling-and-debugging-in-bash
slug: bash-basics-6-error-handling-and-debugging-in-bash
title: "Bash Basics #6: Error Handling and Debugging in Bash"
excerpt: "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."
date: "2024-10-27T09:00:00+01:00"
updated: "2024-11-10T09:00:00+01:00"
author:
  name: "Sebastian Palencsar"
  handle: "spalencsar"
category: "bash-basics"
tags: ["bash", "linux", "shell", "scripting", "debugging", "error-handling", "trap", "basics"]
reading_time: 16
toc: true
---

**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](/en/bash-basics/bash-basics-1-create-your-first-bash-shell-script){.badge-link-text}, work with [variables](/en/bash-basics/bash-basics-2-using-variables-in-bash){.badge-link-text}, use [control structures](/en/bash-basics/bash-basics-3-control-structures-in-bash){.badge-link-text}, [modularize functions](/en/bash-basics/bash-basics-4-functions-in-bash){.badge-link-text}, and [control I/O data streams](/en/bash-basics/bash-basics-5-input-and-output-in-bash){.badge-link-text}.

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*):

```markdown
┌─────────────────────────────────────────────────────────────┐
│                 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

```bash
#!/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:

```bash
#!/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:**
```bash
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:

```bash
#!/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
bash -n deploy_production.sh
```

### 3. Static Analysis with ShellCheck

```bash
# 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*):

```bash
#!/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

<blockquote class="infobox infobox--practice">
❗ **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`).
2. **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.
3. **Task 3 (Custom PS4 Debugger):**
   Adjust `$PS4` so that the current time in format `[HH:MM:SS]` is displayed before each command.
</blockquote>

### Sample Solution for Task 1:

```bash
#!/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](https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap){.badge-link-text} | Official GNU documentation on signal handling with trap |
| [Bash Basics #5: Input and Output in Bash](/en/bash-basics/bash-basics-5-input-and-output-in-bash){.badge-link-text} | The previous part: Data streams, pipes, and here-docs |
| [Linux Command Line Processor Guide](/en/linux-beginners/command-line-processor-in-linux){.badge-link-text} | Fundamental knowledge on shells, I/O streams, and pipes |
| [chmod and File Permissions](/en/linux-beginners/basics-to-best-practices-all-about-chmod-in-linux){.badge-link-text} | 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.

<blockquote class="infobox infobox--info">
💡 **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.
</blockquote>

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](/en/bash-basics/bash-basics-advanced-bash-techniques){.badge-link-text}

👉 **Course overview:** [All lessons of the Bash basics course](/en/category/bash-basics){.badge-link-text}
