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
trapto handle errors and signals - Debug scripts with
bash -xandset -x - Write defensive scripts that validate preconditions
- Understand
set -eedge cases and work around them - Use
shellcheckto 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¶
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:
+ 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:
PS4 controls the trace prefix (default +):
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:
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¶
- What is the difference between
trap 'cmd' ERRandtrap 'cmd' EXIT? When does each fire? - Write a script that takes a lock file, runs for 10 seconds, then exits cleanly — even if you press Ctrl+C.
Interview Questions¶
- What does
trap 'cleanup' EXITdo?
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.
- 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.
- Why might
set -enot stop a script when a command inside anifcondition 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.