opscape

Shell builtins · bash builtin

trap

Run a command when the shell receives a signal or exits, typically to clean up temporary files.

trap [-lp] [[ACTION] SIGNAL...]

10 of 10 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

trap 'commands' SIGNAL... registers code for the shell to run when a signal arrives (INT from Ctrl-C, TERM from kill, HUP when the terminal closes) or at one of bash's pseudo-signals: EXIT when the shell exits for any reason, ERR when a command fails, DEBUG before every command and RETURN when a function returns.

The everyday pattern is cleanup: create a temporary file or directory, then trap 'rm -rf "$tmp"' EXIT immediately, so it is removed whether the script finishes, fails under set -e, or is interrupted. An EXIT trap is the one place to put "always do this at the end".

An action of '' (empty) makes the shell ignore the signal; - restores the default. SIGKILL and SIGSTOP cannot be trapped. Traps are not inherited by subshells and functions in the usual way (see the ERR example and set -E). Traps and signals covers timing and signal-handling details in depth.

Examples

Clean up a temporary file whatever happens

job.sh
#!/usr/bin/env bash
set -e
tmp=$(mktemp)
trap 'rm -f "$tmp"; echo "cleanup: removed $tmp"' EXIT
echo "working in $tmp"
sort data/fruits.txt > "$tmp"
grep -q kiwi "$tmp"
echo "this is not reached"
bash job.sh
echo "exit status: $?"
output
working in /tmp/tmp.R9fwTY8XGP
cleanup: removed /tmp/tmp.R9fwTY8XGP
exit status: 1

grep fails, set -e ends the script, and the EXIT trap still runs and deletes the file. The script's exit status (1, from grep) passes through unchanged.

See the exit code inside an EXIT trap

report.sh
#!/usr/bin/env bash
on_exit() {
  local rc=$?
  if (( rc == 0 )); then echo "finished OK"; else echo "FAILED with status $rc"; fi
}
trap on_exit EXIT
ls "$1" > /dev/null
bash report.sh data
bash report.sh nope; echo "caller sees $?"
output
finished OK
ls: cannot access 'nope': No such file or directory
FAILED with status 2
caller sees 2

At the start of the trap, $? is the status the shell is about to exit with. Save it first thing, before any other command overwrites it. The trap does not change the exit status unless it calls exit itself.

Handle Ctrl-C (SIGINT)

trap 'echo "caught SIGINT, stopping cleanly"; exit 130' INT
echo "working..."
kill -INT $$
echo "not reached"
outputexit 130
working...
caught SIGINT, stopping cleanly

kill -INT $$ sends the shell the same signal Ctrl-C would. The trap runs, prints its message and exits with 130, the conventional status for "killed by SIGINT" (128 + 2).

Ignore a signal

bash -c '
  trap "" TERM
  kill -TERM $$
  echo "still alive after SIGTERM"
  trap - TERM
  kill -TERM $$
  echo "not reached"
'
echo "exit status: $?"
output
still alive after SIGTERM
Terminated
exit status: 143

An empty action makes the inner shell ignore the signal. trap - TERM restores the default, after which the same signal kills it; the outer shell prints Terminated and reports status 143, the convention for "killed by signal 15" (128 + 15).

Report the failing line with an ERR trap

trap 'echo "error: status $? at line $LINENO: $BASH_COMMAND" >&2' ERR
grep -q kiwi data/fruits.txt
ls data > /dev/null
false
echo "the ERR trap does not stop the shell"
output
error: status 1 at line 2: grep -q kiwi data/fruits.txt
error: status 1 at line 4: false
the ERR trap does not stop the shell

The ERR trap runs after every command that fails, under the same rules as set -e. $LINENO and $BASH_COMMAND say where. On its own it only reports; combine it with set -e to also stop.

Make functions inherit the ERR trap with set -E

trap 'echo "ERR trap fired at line $LINENO"' ERR
check() {
  false
  echo "check carries on after false"
  return 3
}
check
echo "--- with set -E:"
set -E
check
echo "done"
output
check carries on after false
ERR trap fired at line 7
--- with set -E:
ERR trap fired at line 3
check carries on after false
ERR trap fired at line 10
done

Without -E, the ERR trap is not inherited by functions: the failing false on line 3 goes unreported and only the failed call on line 7 triggers it. With set -E (errtrace) the trap fires inside the function too (line 3), pointing at the real culprit, and again for the failed call on line 10.

Show active traps with -p

trap 'rm -f /tmp/lock' EXIT
trap 'echo bye' INT TERM
trap -p
output
trap -- 'rm -f /tmp/lock' EXIT
trap -- 'echo bye' SIGINT
trap -- 'echo bye' SIGTERM

trap -p prints every trap as a command that would recreate it. trap -p EXIT shows just one. Note that one trap call can set the same action for several signals.

A second trap on the same signal replaces the first

trap 'echo "first cleanup"' EXIT
trap 'echo "second cleanup"' EXIT
echo "exiting"
output
exiting
second cleanup

Only one action per signal exists; setting another overwrites it. To run several cleanups, put them in one function, or append by reading the current trap with trap -p.

Single quotes delay expansion until the trap runs

file=first.tmp
trap "echo double quotes saw: $file" EXIT
file=second.tmp
trap -p EXIT
trap 'echo single quotes see: $file' EXIT
output
trap -- 'echo double quotes saw: first.tmp' EXIT
single quotes see: second.tmp

With double quotes $file is expanded when the trap is set, freezing first.tmp into it, as trap -p shows. With single quotes the variable is read when the trap fires, so it sees the latest value.

List signal names and numbers

trap -l | head -n 4
explain
output
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP

-l prints the signal numbers and names the shell knows (the first lines shown here). Names can be written with or without the SIG prefix.

Common pitfalls

  • Setting the cleanup trap after creating many resources: if the script dies in between, they leak. Set the trap right after creating the first temporary file.

  • Double quotes in the trap action expand variables when the trap is set, not when it runs. Use single quotes.

  • Reading $? after another command in the trap handler: save it on the first line (local rc=$?).

  • kill -9 (SIGKILL) cannot be trapped, so no cleanup runs. Send TERM first.

  • A trap set inside $(...) or a ( ... ) subshell does not affect the parent shell, and EXIT traps of the parent are reset in subshells.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • trap with signals and EXIT (or 0) is POSIX. ERR, DEBUG, RETURN, -p and -l are bash (and partly ksh/zsh) features; dash only understands real signals and EXIT.

Options worth knowing

A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.

OptionWhat it does
-pPrint the current traps (all, or those for the named signals) in reusable form.
-lList signal names and their numbers.

Related