trap — Signal Handling and Cleanup
The trap built-in lets you run cleanup code when your script exits — whether normally, on error, or from Ctrl+C. Ensure temp files are deleted, locks are released, and status is logged.
January 29, 20267 min read
BashtrapSignalsCleanupScripting
What happens when your script is interrupted with Ctrl+C mid-run? Or crashes with an error? Without traps, temporary files are left behind, locks stay set, and half-finished work corrupts state. trap lets you define a cleanup handler that runs no matter how the script exits.
trap Syntax
trap 'commands' SIGNAL [SIGNAL...]
trap 'commands' EXIT # run when script exits (any reason)
trap 'commands' ERR # run when a command fails (with set -e or alone)
trap 'commands' INT # run on Ctrl+C (SIGINT)
trap 'commands' TERM # run on SIGTERM (kill command default)
trap 'commands' HUP # run on SIGHUP (terminal closed)
# Reset a trap to default behaviour
trap - SIGNAL
# Ignore a signal
trap '' SIGNALEXIT — The Universal Cleanup Trap
The EXIT pseudo-signal runs whenever the script exits, regardless of reason (success, error, or killed with a signal that has a trap). It's the most useful trap for cleanup:
#!/usr/bin/env bash
set -euo pipefail
TMPFILE=$(mktemp)
LOCKFILE="/tmp/myapp.lock"
function cleanup {
echo "Cleaning up..." >&2
rm -f "$TMPFILE"
rm -f "$LOCKFILE"
}
trap cleanup EXIT # cleanup runs no matter how we exit
# Create lock
echo $$ > "$LOCKFILE"
# Do work using temp file
some_command > "$TMPFILE"
process_results "$TMPFILE"
echo "Done"
# cleanup() runs here automaticallyINT and TERM — Handling Interrupts
#!/usr/bin/env bash
function on_interrupt {
echo ""
echo "Interrupted! Shutting down gracefully..." >&2
# stop background processes, close connections, etc.
exit 130 # convention: 128 + SIGINT (2)
}
function on_terminate {
echo "Received SIGTERM — saving state..." >&2
# save state, finish current unit of work
exit 143 # convention: 128 + SIGTERM (15)
}
trap on_interrupt INT
trap on_terminate TERM
echo "Running — press Ctrl+C to stop"
while true; do
sleep 1
echo "tick..."
doneERR — Catching Failures
#!/usr/bin/env bash
set -euo pipefail
function on_error {
local exit_code=$?
local line=${BASH_LINENO[0]}
local command="${BASH_COMMAND}"
echo "ERROR: command '$command' failed with code $exit_code at line $line" >&2
}
trap on_error ERR
echo "Starting..."
cp /nonexistent/file /tmp/ # ← fails, on_error called, then script exits
echo "This never runs"A Complete, Production-Grade Template
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMPDIR_WORK=$(mktemp -d)
function cleanup {
local exit_code=$?
rm -rf "$TMPDIR_WORK"
if [[ $exit_code -ne 0 ]]; then
echo "Script exited with error code: $exit_code" >&2
fi
}
trap cleanup EXIT
function on_error {
echo "Failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2
}
trap on_error ERR
# Main logic
echo "Working in: $TMPDIR_WORK"
# ... do work ...
echo "Success"Quick Check
When does `trap cleanup EXIT` run the `cleanup` function?
Exercise
Write a script with-lock.sh that:
- Creates a lock file
/tmp/myapp.lockcontaining the current PID ($$) - If the lock file already exists, reads the PID from it, checks if that process is still running, and either aborts or clears the stale lock
- Uses
trap ... EXITto always delete the lock file on exit - Sleeps for 10 seconds (simulating work), then exits