Your script creates a temporary directory and starts a process. Halfway through, something fails and the script exits. The temp directory is left behind. The process is still running. Your /tmp is full of garbage from failed runs.
trap is bash’s cleanup mechanism. It says “when this signal happens, run this code.” Use it to delete temp files, kill background processes, and shut down gracefully. It’s not optional — it’s how you write scripts that don’t leave messes.
Basic Trap Syntax
#!/bin/bash
trap 'echo "Cleaning up..."; rm -rf /tmp/mytemp' EXIT
mkdir -p /tmp/mytempecho "Working in /tmp/mytemp"exit 0When the script exits (any exit), the trap runs: cleanup code executes, temp directory is deleted.
$ bash basic_trap.shWorking in /tmp/mytempCleaning up...
$ ls /tmp/mytempls: cannot access '/tmp/mytemp': No such file or directoryThe trap fired and cleaned up. Perfect.
Common Signals
EXIT — Runs when the script exits, for any reason (normal or error):
#!/bin/bash
trap 'echo "Script ended"' EXIT
echo "Starting"false # Errorecho "Never runs"$ bash trap_exit.shStartingScript endedEXIT fires even though the script errored out.
INT — Runs when user presses Ctrl+C:
#!/bin/bash
trap 'echo "Interrupted"; exit 1' INT
echo "Press Ctrl+C to interrupt..."while true; do sleep 1done$ bash trap_int.shPress Ctrl+C to interrupt...^CInterruptedUser pressed Ctrl+C, INT trap fired, script exited cleanly.
TERM — Runs when the script receives SIGTERM (from kill):
#!/bin/bash
trap 'echo "Terminating gracefully"; cleanup_function' TERM
cleanup_function() { echo "Cleaning up..." kill $bg_pid 2>/dev/null || true}
echo "Starting background work..."sleep 100 &bg_pid=$!waitIn another terminal:
$ kill -TERM <pid>Script receives SIGTERM, trap fires, cleanup runs, script exits.
ERR — Runs when any command exits with non-zero status:
#!/bin/bashset -E # Enable ERR trap inheritance
trap 'echo "Error on line $LINENO"; exit 1' ERR
echo "Starting"false # Errorecho "Never runs"$ bash trap_err.shStartingError on line 6Real-World Pattern: Complete Cleanup
#!/bin/bashset -euo pipefail
TEMP_DIR=""BG_PIDS=()
cleanup() { local exit_code=$?
echo "Cleaning up..."
# Kill background processes for pid in "${BG_PIDS[@]}"; do if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi done
# Remove temp directory if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then rm -rf "$TEMP_DIR" fi
exit "$exit_code"}
trap cleanup EXIT INT TERM
# Create temp directoryTEMP_DIR=$(mktemp -d)echo "Using temp dir: $TEMP_DIR"
# Start background workecho "Starting background task..."sleep 100 &BG_PIDS+=($!)
echo "Waiting for background tasks..."wait
echo "Done"Breakdown:
cleanup()is called on EXIT, INT, or TERM- Kills all tracked background processes
- Removes temp directory
- Exits with the original exit code
Trap Multiple Signals
Use one trap function for multiple signals:
#!/bin/bash
cleanup() { echo "Caught signal: exiting gracefully" rm -f /tmp/lock exit 0}
trap cleanup EXIT INT TERM
echo "Running... press Ctrl+C"while true; do sleep 1doneOr set separate traps:
#!/bin/bash
trap 'echo "Normal exit"; rm -f /tmp/lock' EXITtrap 'echo "Interrupted"; exit 1' INTtrap 'echo "Terminated"; exit 1' TERM
while true; do sleep 1doneError Handling with trap ERR
Instead of relying on set -e (which has gotchas):
#!/bin/bashset -u
trap 'echo "Error on line $LINENO"; exit 1' ERR
echo "Step 1"false # Errorecho "Never runs"$ bash trap_err_safe.shStep 1Error on line 7This is more reliable than set -e because it fires on actual errors.
Combining with trap Debugging
#!/bin/bash
trap 'echo "Line $LINENO: $BASH_COMMAND"' DEBUGtrap 'echo "Error on line $LINENO"; exit 1' ERR
echo "Step 1"ls /nonexistentecho "Never runs"$ bash trap_debug.shLine 7: echo "Step 1"Step 1Line 8: ls /nonexistentError on line 8DEBUG trap runs before every command. Useful for tracing execution.
Trap in Functions
Traps set in a function apply globally:
#!/bin/bash
setup_traps() { trap 'echo "Function ended"' EXIT}
setup_traps
echo "Main script"$ bash trap_in_func.shMain scriptFunction endedThe trap set inside the function fires for the whole script.
The Pipeline Trap Pattern
For scripts that involve pipelines, lock files, or temporary state:
#!/bin/bashset -euo pipefail
LOCK_FILE="/tmp/myapp.lock"LOG_FILE="/tmp/myapp.log"
cleanup() { local exit_code=$? echo "Cleanup: removing lock and flushing logs" >&2 rm -f "$LOCK_FILE" [ -f "$LOG_FILE" ] && tail -20 "$LOG_FILE" >&2 exit "$exit_code"}
trap cleanup EXIT INT TERM
# Acquire lockif [ -f "$LOCK_FILE" ]; then echo "Another instance is running" >&2 exit 1fitouch "$LOCK_FILE"
# Do work, log everything{ echo "Starting work: $(date)" sleep 2 echo "Work complete: $(date)"} | tee "$LOG_FILE"
echo "Pipeline successful"On exit, cleanup removes the lock and shows the last 20 lines of the log.
The One Thing
Any script that:
- Creates temp files or directories
- Starts background processes
- Requires cleanup on failure
- Needs graceful shutdown
Should have at least:
#!/bin/bashset -euo pipefail
cleanup() { echo "Cleaning up..." # Delete temp files # Kill background pids # Release locks}
trap cleanup EXIT INT TERM
# ... rest of scriptThat pattern is your baseline for production-ready bash. No exceptions.