0Pricing
DevOps Bootcamp · Lesson

Trap Handlers for Cleanup and Signals

Register EXIT, ERR, and INT traps to remove temp files and roll back partial work reliably.

Trap Handlers for Cleanup and Signals is a free DevOps Bootcamp lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Trap Handlers Matter

When a Bash script exits — whether normally, due to an error, or because the user pressed Ctrl+C — it can leave behind temporary files, half-written data, or broken state. Without cleanup, these artifacts accumulate and cause hard-to-debug problems.

Trap handlers solve this by registering a function or command that Bash executes automatically when a specific signal or pseudo-signal is received.

  • EXIT — fires whenever the script exits, for any reason
  • ERR — fires after any command returns a non-zero exit status
  • INT — fires when the user sends SIGINT (Ctrl+C)
  • TERM — fires on SIGTERM (e.g. from kill)

A well-written script registers these traps at the very top, before any risky work begins.

The trap Builtin Syntax

The trap builtin registers a handler for one or more signals or pseudo-signals. The general syntax is:

trap 'command_or_function' SIGNAL [SIGNAL...]

Key rules:

  • The first argument is a quoted string of shell code (or a function name) to execute when the signal fires.
  • You can list multiple signals after the handler.
  • trap '' SIGNAL ignores that signal (empty handler).
  • trap - SIGNAL resets the signal to its default behavior.

Traps are inherited by functions called in the same shell, but not by subshells spawned with ( ) or &.

#!/usr/bin/env bash
# Syntax examples — not a full script

# Register a cleanup function on EXIT
trap cleanup EXIT

# Inline handler for INT and TERM
trap 'echo "Interrupted!"; exit 1' INT TERM

# Ignore SIGHUP
trap '' HUP

# Reset SIGPIPE to default
trap - PIPE

The EXIT Trap — Your Safety Net

The EXIT pseudo-signal is the most important trap to register. It fires when the shell exits for any reason: normal completion, exit N, an unhandled error, or a signal. This makes it ideal for removing temporary files.

Best practice:

  • Create temp files with mktemp so the name is unique and unpredictable.
  • Store the path in a variable immediately.
  • Register the EXIT trap right after creation so cleanup always runs.

The exit code of the script is preserved — the trap handler's exit code is ignored unless you explicitly call exit inside it.

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

TMPFILE=$(mktemp /tmp/myapp.XXXXXX)

cleanup() {
    echo "Removing temp file: $TMPFILE" >&2
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Do real work
echo "Processing..." > "$TMPFILE"
sort "$TMPFILE" -o "$TMPFILE"
cp "$TMPFILE" /tmp/myapp_result.txt

echo "Done. Result saved."

Handling Multiple Temp Files and Directories

Real scripts often create multiple temporary artifacts — files, directories, FIFOs, lock files. Instead of tracking each one separately, collect them in an array and remove everything in a single cleanup function.

This pattern scales cleanly: every time you create a new temp resource, append it to the array. The EXIT trap always handles the full list regardless of where the script exits.

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

CLEANUP_TARGETS=()

add_cleanup() {
    CLEANUP_TARGETS+=("$1")
}

cleanup() {
    echo "Running cleanup..." >&2
    for target in "${CLEANUP_TARGETS[@]:-}"; do
        rm -rf "$target"
        echo "Removed: $target" >&2
    done
}
trap cleanup EXIT

# Create and track temp resources
TMPDIR_WORK=$(mktemp -d /tmp/work.XXXXXX)
add_cleanup "$TMPDIR_WORK"

TMPFILE_LOG=$(mktemp /tmp/run.XXXXXX.log)
add_cleanup "$TMPFILE_LOG"

echo "Working in $TMPDIR_WORK" | tee "$TMPFILE_LOG"
touch "$TMPDIR_WORK/output.txt"
echo "Step complete."

The ERR Trap — Catching Failures

The ERR pseudo-signal fires after any simple command returns a non-zero exit status, provided the shell option set -e (errexit) is active or the command is not inside an if, while, or ||/&& compound.

Use the ERR trap to:

  • Log which line failed using $LINENO and $BASH_COMMAND.
  • Trigger rollback logic before the EXIT trap runs.
  • Emit structured error messages to stderr.

Important: ERR traps do not fire inside functions unless you explicitly run set -E (errtrace) so that functions inherit the ERR trap from the calling shell.

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

on_error() {
    local exit_code=$?
    local line=$1
    echo "ERROR: command failed with exit code $exit_code at line $line" >&2
    echo "  Command: $BASH_COMMAND" >&2
}
trap 'on_error $LINENO' ERR

echo "Starting task..."
cp /nonexistent/file /tmp/   # This will fail
echo "This line is never reached."

The INT Trap — Graceful Ctrl+C

Pressing Ctrl+C sends SIGINT to the foreground process group. Without a trap, Bash exits immediately, possibly leaving partial work behind.

Registering an INT trap lets you:

  • Print a user-friendly cancellation message.
  • Perform any intermediate rollback before handing off to the EXIT trap.
  • Exit with a meaningful status code (130 is the conventional code for SIGINT: 128 + 2).

After your INT handler finishes, call exit 130 to propagate the correct exit code. The EXIT trap will then run automatically.

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

TMPFILE=$(mktemp /tmp/demo.XXXXXX)

cleanup() {
    rm -f "$TMPFILE"
    echo "Temp file removed." >&2
}

on_interrupt() {
    echo "" >&2
    echo "Caught SIGINT — cancelling gracefully." >&2
    exit 130   # EXIT trap (cleanup) runs automatically after this
}

trap cleanup EXIT
trap on_interrupt INT

echo "Running long task. Press Ctrl+C to cancel."
for i in $(seq 1 10); do
    echo "Step $i/10..."
    sleep 1
done
echo "All steps complete."

Combining EXIT, ERR, and INT Traps

In production scripts, you typically register all three traps together to cover every exit path:

  • EXIT — always runs; handles file cleanup.
  • ERR — logs the failing command and line number.
  • INT — prints a cancellation message and exits with code 130.

Because EXIT always runs last, your cleanup logic only needs to live in one place. ERR and INT handlers can focus on logging and setting state, then delegate the actual cleanup to EXIT by calling exit.

Use set -eEuo pipefail at the top so that errors in functions also trigger ERR, and pipefail catches failures inside pipelines.

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

TMPFILE=$(mktemp /tmp/combined.XXXXXX)
ROLLBACK_NEEDED=false

cleanup() {
    if [[ "$ROLLBACK_NEEDED" == true ]]; then
        echo "Rolling back partial changes..." >&2
    fi
    rm -f "$TMPFILE"
    echo "Cleanup done." >&2
}

on_error() {
    ROLLBACK_NEEDED=true
    echo "ERR at line $1: $BASH_COMMAND" >&2
}

on_interrupt() {
    echo "Interrupted by user." >&2
    exit 130
}

trap cleanup       EXIT
trap 'on_error $LINENO' ERR
trap on_interrupt  INT

echo "hello" > "$TMPFILE"
echo "Script finished successfully."

Resetting and Disabling Traps Dynamically

Sometimes you need to modify a trap mid-script — for example, a one-time cleanup after a critical section completes, so subsequent normal operations do not trigger unnecessary rollback.

  • trap - SIGNAL resets the signal to its built-in default behavior.
  • trap '' SIGNAL ignores the signal entirely (the process cannot be killed by it).
  • You can re-register a new handler at any point; the last trap call wins.

A common pattern is to upgrade the EXIT trap once a risky operation succeeds, switching from a rollback handler to a simple cleanup handler.

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

TMPFILE=$(mktemp /tmp/staged.XXXXXX)

rollback() {
    echo "ROLLBACK: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

cleanup_only() {
    echo "CLEANUP: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

# Start with rollback in case something fails during the risky phase
trap rollback EXIT

echo "Performing risky operation..."
echo "critical data" > "$TMPFILE"
# ... imagine more risky steps here ...

# Risky phase succeeded — switch to simple cleanup
trap cleanup_only EXIT

echo "Risky phase done. Now doing safe finalization."
cp "$TMPFILE" /tmp/staged_result.txt
echo "All done."

Propagating Exit Codes Through Traps

A subtle pitfall: if your cleanup function contains commands that can fail, the script's final exit code may be overwritten. Preserve the original exit code explicitly.

Inside a trap handler, $? holds the exit code that triggered the trap. Capture it at the very first line of the handler before any other command changes it.

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

TMPFILE=$(mktemp /tmp/exitcode.XXXXXX)

cleanup() {
    local original_exit=$?   # capture BEFORE any other command
    echo "Cleaning up (original exit: $original_exit)" >&2
    rm -f "$TMPFILE"
    # Exit with the original code so callers see the real result
    exit "$original_exit"
}
trap cleanup EXIT

echo "Doing work..."
echo "data" > "$TMPFILE"

# Simulate failure
if [[ ! -f /tmp/required_marker ]]; then
    echo "Required marker missing!" >&2
    exit 2
fi

echo "Done."

Traps in Functions and Subshells

Understanding trap inheritance is critical for writing correct scripts:

  • Functions run in the same shell and inherit traps — but ERR only propagates to functions when set -E (errtrace) is active.
  • Subshells (( ... )) start with a copy of the parent's traps, but changes inside the subshell do not affect the parent.
  • Background jobs (cmd &) are subshells and do not inherit the parent's INT/TERM traps (they get the default signal disposition).

Practical rule: keep your trap registration in the main script body, use set -E when you need ERR to propagate into functions, and never rely on trap inheritance across &.

#!/usr/bin/env bash
set -eEuo pipefail   # -E = errtrace: ERR propagates into functions

on_error() {
    echo "ERR in function or main at line $1" >&2
}
trap 'on_error $LINENO' ERR

risky_function() {
    echo "Inside risky_function"
    ls /nonexistent_path   # triggers ERR — works because of set -E
}

# Subshell: has its own copy; changes don't affect parent
(
    trap '' ERR   # disable ERR only inside subshell
    ls /nonexistent_path 2>/dev/null || true
    echo "Subshell completed without triggering parent ERR"
)

risky_function

Real-World Pattern: Atomic File Replacement

A classic defensive Bash pattern is atomic file replacement: write to a temp file, then rename it into place. If anything fails before the rename, the original is untouched. Traps make this bulletproof.

Steps:

  1. Create a temp file in the same filesystem as the destination (so mv is atomic).
  2. Register EXIT trap to remove the temp file if we bail early.
  3. Write and validate the new content.
  4. Atomically rename (mv) — only now is the original replaced.
  5. The EXIT trap removes the temp only if it still exists (after a successful mv, it is gone).
#!/usr/bin/env bash
set -euo pipefail

DEST="/tmp/important_config.conf"
TMPFILE=$(mktemp "$(dirname "$DEST")/.tmp.XXXXXX")

cleanup() {
    rm -f "$TMPFILE"   # no-op if mv already moved it
}
trap cleanup EXIT

# Write new content to temp file
cat > "$TMPFILE" <<'EOF'
[settings]
version=2
mode=production
EOF

# Validate before replacing
if ! grep -q 'version=' "$TMPFILE"; then
    echo "Validation failed — aborting replacement" >&2
    exit 1
fi

# Atomic rename — DEST is replaced only here
mv "$TMPFILE" "$DEST"
echo "Config updated atomically: $DEST"

Knowledge Check: ERR Trap Inheritance

Test your understanding of how the ERR trap behaves with functions in Bash.

Recap: Trap Handlers for Cleanup and Signals

In this lesson you learned how to make Bash scripts resilient through trap handlers:

  • EXIT is your universal safety net — register it immediately after creating any temp resource and always capture $? first to preserve the original exit code.
  • ERR lets you log failures with $LINENO and $BASH_COMMAND; use set -E so functions inherit it.
  • INT handles Ctrl+C gracefully — print a message and exit with code 130 to let EXIT clean up.
  • Collect temp resources in an array and remove them all in one cleanup function.
  • Use trap - SIGNAL to reset and trap '' SIGNAL to ignore signals dynamically.
  • The atomic rename pattern (mktemp → write → validate → mv) combined with an EXIT trap guarantees that partial writes never corrupt the destination file.

Combining set -eEuo pipefail with well-placed traps gives you a solid defensive foundation for any production Bash script.

Frequently asked questions

Is the “Trap Handlers for Cleanup and Signals” lesson free?

Yes — the full text of “Trap Handlers for Cleanup and Signals” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.

What will I learn in “Trap Handlers for Cleanup and Signals”?

Register EXIT, ERR, and INT traps to remove temp files and roll back partial work reliably. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start DevOps Bootcamp?

No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Trap Handlers for Cleanup and Signals” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this DevOps Bootcamp lesson?

Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Strict Mode with set -euo pipefail
  2. Trap Handlers for Cleanup and Signals
  3. Safe Temporary Files and Lock Directories
  4. Idempotent Scripts and Retry-with-Backoff Logic
← Back to DevOps Bootcamp