Skip to content

Error Handling & Debugging

Scripts that fail silently are worse than scripts that do not exist — proper error handling means you know when something went wrong and why.

Learning Objectives

  • Use trap to handle errors and signals
  • Debug scripts with bash -x and set -x
  • Write defensive scripts that validate preconditions
  • Understand set -e edge cases and work around them
  • Use shellcheck to catch bugs before running

trap — Respond to Signals and Errors

trap lets you run cleanup code when a script exits, errors, or receives a signal.

Cleanup on Exit

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

TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT    # runs whenever the script exits

# Use $TMPFILE freely — it will always be cleaned up
echo "data" > "$TMPFILE"
process_data "$TMPFILE"

EXIT fires on any exit: normal completion, exit N, or error. Use it for cleanup — temp files, lock files, restoring state.

Error Trap

trap 'echo "ERROR: line $LINENO" >&2' ERR    # fires on any non-zero exit

# More useful version with context
trap 'echo "ERROR at line $LINENO: $(sed -n "${LINENO}p" "$0")" >&2' ERR

Signal Traps

trap 'echo "Interrupted. Cleaning up..."; exit 130' INT TERM

Common signals: INT (Ctrl+C), TERM (kill), HUP (terminal close), EXIT (any exit).

Trap first, allocate resources second

Set up your trap EXIT before creating any resources (temp files, lock files) to guarantee cleanup even if the script is interrupted immediately after creation.


Debugging with bash -x

bash -x prints each command before executing it, with a + prefix:

bash -x myscript.sh
+ name=Nikhil
+ echo 'Hello, Nikhil'
Hello, Nikhil
+ [-f /etc/passwd](<../-f /etc/passwd.md>)
+ wc -l /etc/passwd
45 /etc/passwd

Enable/disable debug output inside a script:

set -x    # start tracing
cp "$src" "$dest"
set +x    # stop tracing

PS4 controls the trace prefix (default +):

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

Defensive Precondition Checks

Check everything that can go wrong at the start of the script:

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

# Check required tools
for tool in rsync curl jq; do
    command -v "$tool" >/dev/null 2>&1 || {
        echo "Error: '$tool' is required but not installed" >&2
        exit 1
    }
done

# Check required arguments
[[ $# -ge 1 ]] || { echo "Usage: $0 <input>" >&2; exit 1; }

# Check required environment variables
: "${AWS_PROFILE:?AWS_PROFILE must be set}"

# Check required files
[[ -f "$1" ]] || { echo "File not found: $1" >&2; exit 1; }

The die Pattern

Centralize error messages:

die() {
    echo "ERROR: ${BASH_SOURCE[1]}:${BASH_LINENO[0]}: $*" >&2
    exit 1
}

[[ -d "$BACKUP_DIR" ]] || die "Backup directory does not exist: $BACKUP_DIR"

ShellCheck

ShellCheck is a static analysis tool that finds bugs before you run the script:

shellcheck myscript.sh
In myscript.sh line 12:
for f in $(ls *.txt); do
         ^-----------^ SC2045: Iterating over ls output is fragile.
                               Use globs instead.

Install: apt install shellcheck / brew install shellcheck.

Run ShellCheck on every script before committing

ShellCheck catches quoting bugs, word-splitting issues, unused variables, and portability problems that even experienced bash programmers miss. Make it part of your workflow from the start.


Common Patterns

Locking (prevent concurrent runs)

LOCKFILE="/tmp/${SCRIPT_NAME}.lock"
exec 9>"$LOCKFILE"
flock -n 9 || { echo "Another instance is running"; exit 1; }
trap 'rm -f "$LOCKFILE"' EXIT

Logging to file and terminal

LOGFILE="/var/log/myscript.log"
exec > >(tee -a "$LOGFILE") 2>&1
echo "Script started"    # goes to both terminal and log file

Practice Exercises

Main (write a short script)

Rewrite any script from Week 01 to include: - trap cleanup EXIT that removes any temp files - A die() function for error messages - Precondition checks at the top - bash -x tracing when --debug is passed

Stretch

  1. What is the difference between trap 'cmd' ERR and trap 'cmd' EXIT? When does each fire?
  2. Write a script that takes a lock file, runs for 10 seconds, then exits cleanly — even if you press Ctrl+C.

Interview Questions

  1. What does trap 'cleanup' EXIT do?
Show answer

It registers a command to run whenever the script exits — whether by reaching the end, an exit statement, or an unhandled error. It is the standard way to clean up temporary files, lock files, or other resources in bash scripts.

  1. How do you debug a bash script without adding print statements?
Show answer

Run it with bash -x script.sh to trace every command. Set PS4 for more context (file, line number, function name). Use set -x / set +x to trace specific sections. bash -n script.sh checks syntax without running anything.

  1. Why might set -e not stop a script when a command inside an if condition fails?
Show answer

-e does not trigger for commands that are evaluated as a condition — i.e., in the test position of if, while, until, or after &&/||. The rationale is that the script is explicitly testing for failure in these positions. Commands that genuinely need to fail-stop must not be used in conditional positions if you rely on -e.


day01-part2-script-structure | day02-part2-string-arrays