Sagar.BlogArticle
All posts
All posts
Bash

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 '' SIGNAL

EXIT — 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 automatically

INT 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..."
done

ERR — 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:

  1. Creates a lock file /tmp/myapp.lock containing the current PID ($$)
  2. 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
  3. Uses trap ... EXIT to always delete the lock file on exit
  4. Sleeps for 10 seconds (simulating work), then exits