Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 9 – Robust scripts: errors, traps and debugging

Traps and cleanup: EXIT, ERR and signals

Register cleanup code with trap, see when an EXIT trap runs and when it cannot, handle INT and TERM gracefully, and tell ERR, RETURN and DEBUG apart.

  • Intermediate
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Install cleanup code that runs however a Bash script exits, and name the one signal it misses
  • Handle INT and TERM so a script finishes its current item and reports 130 or 143
  • Use ERR, RETURN and DEBUG traps for diagnostics, and know which need set -E or set -T
  • Write trap actions in single quotes so variables expand at trap time, not at trap-set time

Before you start

What you’ll learn · Before you start

What you will learn

  • Install cleanup code that runs however a Bash script exits, and name the one signal it misses
  • Handle INT and TERM so a script finishes its current item and reports 130 or 143
  • Use ERR, RETURN and DEBUG traps for diagnostics, and know which need set -E or set -T
  • Write trap actions in single quotes so variables expand at trap time, not at trap-set time

Before you start

On this page

A batch job wrote its progress to ./workspace, crashed on a bad line of input, and left the folder behind. The next run opened the folder with yesterday’s half-finished files inside and treated them as its own, double-counting every bill. The missing line was one: a cleanup trap that runs however the script ends.

You will register code to run on exit with the trap builtin, learn the one way out that still skips it, catch SIGINT and SIGTERM so a worker finishes its current item and reports the right status, use ERR, RETURN and DEBUG for diagnostics, and single-quote the trap action so variables expand when the trap runs.

The trap builtin registers code to run on a signal

The trap: A Bash builtin that registers code to run on a signal or a shell event: EXIT when the shell exits, ERR when a command's failure would end a set -e script, RETURN when a function returns, DEBUG before every simple command. trap action signal …; the action is one shell word. builtin says “when this signal reaches the shell, run that code”. Its two arguments are the action, as one shell word, and one or more signal names (or EXIT, the shell’s own event). trap is one of POSIX’s special built-ins, so every POSIX shell has it, and the Bash manual describes it with the Bourne shell builtins.

The usual way to pair it with a temporary folder is mktemp -d and a cleanup action:

mktemp -d paired with an EXIT trap that removes the folder Shell · mktemp_pattern.sh
#!/bin/bash
# Pair mktemp -d with an EXIT trap so the folder is removed however the shell ends.
# Read-only snippet; a real script runs this at the top.
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT

The lesson’s examples use a fixed folder name so the recorded output is identical from run to run; a real script writes work=$(mktemp -d) and the trap removes it. tmp_cleanup_session.py writes three small scripts and runs them, then runs tmp_term.sh in Bash and in dash (the next section comes back to those two runs):

tmp_normal.sh and tmp_errors.sh clean up, tmp_quoted_wrong.sh does not, and tmp_term.sh in Bash and dash Python · tmp_cleanup_session.py
from session import run, write

# An EXIT trap paired with a temp folder is the usual way to make sure the folder is removed however the shell ends:
# a normal exit, a false under set -e, or a Ctrl-C from outside. The lesson's examples use a fixed name so the
# output is identical across runs; a real script writes "work=$(mktemp -d)" instead.
write("tmp_normal.sh", """#!/bin/bash
set -euo pipefail
work=./workspace
mkdir -p "$work"
trap 'rc=$?; rm -rf -- "$work"; echo "cleaned $work (rc=$rc)"' EXIT

touch -- "$work/report.json"
ls -- "$work"
echo "work done"
""")

write("tmp_errors.sh", """#!/bin/bash
set -euo pipefail
work=./workspace
mkdir -p "$work"
trap 'rc=$?; rm -rf -- "$work"; echo "cleaned $work (rc=$rc)"' EXIT

touch -- "$work/report.json"
echo "about to fail"
false
echo "unreachable"
""")

write("tmp_quoted_wrong.sh", """#!/bin/bash
# The classic double-quote slip: $work was empty when the trap was set, so the stored action is
# rm -rf -- ; echo "cleaned []" and rm gets no operand at all. Single-quote the action so the expansion happens
# when the trap runs, not when it is set.
work=
trap "rm -rf -- $work; echo \\"cleaned [$work]\\"" EXIT

work=./workspace
mkdir -p "$work"
touch -- "$work/leftover"
echo "after the script, the folder named $work still exists"
""")

# A TERM from outside, simulated by the script sending it to itself: the script sets no TERM trap at all.
write("tmp_term.sh", """#!/bin/bash
work=./workspace
mkdir -p "$work"
trap 'rm -rf -- "$work"; echo "cleaned $work"' EXIT
echo "working when TERM arrives"
kill -TERM $$
echo "unreachable"
""")

run("""
chmod +x tmp_normal.sh tmp_errors.sh tmp_quoted_wrong.sh tmp_term.sh
bash tmp_normal.sh
!bash tmp_errors.sh
bash tmp_quoted_wrong.sh
echo "leftover check: $(ls workspace/ 2>/dev/null)"
rm -rf ./workspace
!bash tmp_term.sh
echo "after bash: $(ls -d workspace 2>/dev/null || echo 'no workspace left')"
!dash tmp_term.sh
echo "after dash: $(ls -d workspace 2>/dev/null || echo 'no workspace left')"
rm -rf ./workspace
""")

Output

$ chmod +x tmp_normal.sh tmp_errors.sh tmp_quoted_wrong.sh tmp_term.sh
$ bash tmp_normal.sh
report.json
work done
cleaned ./workspace (rc=0)
$ bash tmp_errors.sh
about to fail
cleaned ./workspace (rc=1)
[exit status 1]
$ bash tmp_quoted_wrong.sh
after the script, the folder named ./workspace still exists
cleaned []
$ echo "leftover check: $(ls workspace/ 2>/dev/null)"
leftover check: leftover
$ rm -rf ./workspace
$ bash tmp_term.sh
working when TERM arrives
cleaned ./workspace
[exit status 143]
$ echo "after bash: $(ls -d workspace 2>/dev/null || echo 'no workspace left')"
after bash: no workspace left
$ dash tmp_term.sh
working when TERM arrives
[exit status 143]
$ echo "after dash: $(ls -d workspace 2>/dev/null || echo 'no workspace left')"
after dash: workspace
$ rm -rf ./workspace

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tmp_cleanup_session.py

tmp_normal.sh finished its last line, the EXIT trap ran, printed cleaned ./workspace (rc=0), and the shell exited 0. tmp_errors.sh hit false under set -e, the EXIT trap still ran with rc=1 from the saved exit status, and the folder was gone before the shell left. The third one is a slip: with the action in double quotes, the $work inside expanded when the trap was set, while it was still empty, so the stored action was rm -rf -- ; echo "cleaned []", an rm with no operand at all. After the script the folder was still there, as the leftover check line showed. The section after next is about exactly that.

Checkpoint

Check: The trap builtin and its two arguments

  1. One question What are the two arguments trap takes, in order?

    Choose one answer.

    Show the answer: One question

    Answer: The action to run, as one shell word, and one or more signal names (or EXIT, ERR, RETURN, DEBUG)

    Multiple names on one trap register the same action for each of them: trap cleanup EXIT INT TERM.

An EXIT trap runs however the script ends, except on SIGKILL

The EXIT pseudo-signal is the useful one. The Bash manual says the EXIT trap is executed when the shell exits, and in Bash 5.3 that means every way out but one. Step through five ways a script can end:

Interactive · step by step An EXIT trap runs however a script ends Step through five ways a Bash script ends and see which of them let the EXIT trap run.
An EXIT trap runs however a script ends, step 6 of 6. Showing trap cleanup EXIT, Runs to the end, set -e stops it, Ctrl-C sends INT, kill sends TERM, SIGKILL arrives, Cleanup runs, Nothing runs. In focus: SIGKILL arrives, Nothing runs.trap cleanup EXITset near the topRuns to the endor calls exitset -e stops itunhandled failureCtrl-C sends INTno INT trap setkill sends TERMno TERM trap setSIGKILL arriveskill -9, OOM killerCleanup runsthen the shell exitsNothing runsthe folder is left

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The trap is set. trap cleanup EXIT tells Bash to run cleanup when this shell exits. Setting it costs nothing; what matters is which ways out count as exits.
  2. Runs to the end. The usual case: the shell finishes its last command, runs the EXIT trap, and leaves with the status it had before the trap. tmp_normal.sh printed "cleaned ./workspace (rc=0)".
  3. set -e stops it. An unhandled non-zero ends a set -e script, and the EXIT trap still runs on the way out. tmp_errors.sh printed "cleaned ./workspace (rc=1)" and ended with status 1.
  4. Ctrl-C sends INT. With no INT trap, the signal ends the script with status 130 (128 + 2), and Bash 5.3 runs the EXIT trap first. A handler that lets the script finish its item comes in the next section.
  5. kill sends TERM. tmp_term.sh sent itself TERM with no TERM trap set: Bash ran the EXIT trap, printed "cleaned ./workspace" and ended with 143. dash 0.5.13.5 ran no EXIT trap for the same script and left the folder behind.
  6. SIGKILL leaves nothing to run. kill -9 and the Linux OOM killer send SIGKILL, which cannot be caught, blocked or ignored (signal(7)). The process ends at once and no trap runs, so anything the script was meant to clean up stays.

The one it misses is SIGKILL (kill -9 and the Linux Out-Of-Memory killer). signal(7) says it cannot be caught, blocked or ignored: the process ends at once, and no shell code runs. Everything else runs the trap in Bash: a plain exit, a false that set -e catches, a Ctrl-C that becomes SIGINT, and a kill -TERM from outside, which the session’s tmp_term.sh showed by sending itself TERM with no TERM trap set. That is Bash’s behaviour, not a rule of POSIX: dash 0.5.13.5 ran no EXIT trap for the same script and left the folder behind, so a script written for sh also traps INT and TERM and exits from there (trap 'exit 143' TERM).

The rc=$? on the first line of a cleanup action is a small habit that pays back. Without it, the next command in the trap resets $? to its own status; with it, the trap sees the status that caused the exit and can log it. The tmp_errors.sh run printed cleaned ./workspace (rc=1) only because the trap started with rc=$?.

Key idea

Put the cleanup in an EXIT trap, not on the “happy path”: Bash runs it however the script ends (the last line, a set -e stop, a Ctrl-C, an external TERM), and the one way out that skips it, SIGKILL, is also the only one that no code in the script can handle.

Checkpoint

Check: When an EXIT trap fires, and the one time it does not

  1. One question Which of these ends of a Bash 5.3 script let its EXIT trap run?

    Choose every answer that is right.

    Show the answer: One question

    Answer:

    • set -e sees an unhandled non-zero
    • A Ctrl-C that becomes SIGINT and the shell has no INT trap
    • The script reaches its last line

    "Register cleanup in EXIT and nowhere else" covers four of five paths; SIGKILL and a hard power cut are the exceptions, which is why cleanups must also be safe to re-run from the outside.

Single-quote the trap action, so variables expand at trap time

The third script in the previous session showed the slip. The action is one shell word, and Bash reads it like any other string: with double quotes, every $name is expanded now, when the trap is being set; with single quotes the string reaches trap as it was written, and the expansion happens when the trap fires. ShellCheck’s SC2064 names the slip and the fix.

Double-quoted action freezes $work at trap-set time; single quotes defer the expansion Shell · trap_quoting.sh
#!/bin/bash
# Trap action quoting: the string reaches `trap` once, and its expansions follow the quoting style.
# Read-only snippet.

# Wrong: $work expands now; the trap stores the current value.
trap "rm -rf -- $work" EXIT

# Right: $work expands when the trap runs, which is after the script has set $work.
trap 'rm -rf -- "$work"' EXIT

The same rule covers $LINENO, $BASH_COMMAND and $? inside an ERR or DEBUG trap: single-quote the action so each one reads the value that is current at trap time. A multi-line action is best written as a function and registered by name:

A multi-line cleanup lives in a function; the trap registers it by name Shell · cleanup_function.sh
#!/bin/bash
# A multi-line cleanup lives in a function; the trap registers it by name.
# Read-only snippet; $work and $pidfile come from the surrounding script.
cleanup() {
    local rc=$?
    rm -rf -- "$work"
    [ -n "$pidfile" ] && rm -f -- "$pidfile"
    echo "exited with $rc" >&2
}
trap cleanup EXIT

A function also keeps the trap line short and puts the cleanup where it can be read, tested and changed like the rest of the script.

Checkpoint

Check: Single-quote the trap action for trap-time expansion

  1. One question A script runs trap "rm -rf -- $work" EXIT with $work empty, and later sets work=./sandbox. What runs at exit?

    Choose one answer.

    Show the answer: One question

    Answer: rm -rf --, with no path: nothing is removed, and $work is still there

    ShellCheck SC2064 names this slip and prescribes single quotes; a function registered by name (trap cleanup EXIT) is often even clearer.

Catching INT and TERM, then exiting 130 or 143

SIGINT (number 2 on Linux and on macOS, written INT to trap) and SIGTERM (15, TERM) are the two polite “please stop” signals. Without a trap, Bash ends the script and reports 130 for INT (128 + 2) or 143 for TERM (128 + 15). With a trap, the handler runs — and it must call exit to leave; otherwise, after the handler returns, the shell goes back to what it was doing. A worker that finishes its current item and exits 143 is the standard form:

A worker that catches TERM, finishes the item on which it was interrupted and exits 143 Python · graceful_worker_session.py
from session import run, write

# A worker that handles TERM, finishes the item it is on, cleans up and exits 143 (128 + 15, for SIGTERM).
# For a reproducible session, the script sends TERM to itself after the second item so no second shell is needed.
# In production, the TERM would come from a scheduler, a service manager stopping the job, or `kill` from an admin.
write("worker.sh", """#!/bin/bash
set -euo pipefail

STOP=0
cleanup() {
    local rc=$?
    rm -f -- state.json
    echo "cleaned state.json (rc=$rc)"
}
trap cleanup EXIT
trap 'STOP=1; echo "received TERM at item $i"' TERM

for i in 1 2 3 4; do
    echo "processing item $i"
    printf '{"last": %d}\\n' "$i" > state.json
    # Simulate an external 'kill -TERM' arriving while we are between items 2 and 3.
    if [ "$i" = 2 ]; then
        kill -TERM $$
    fi
    if (( STOP == 1 )); then
        echo "stopping gracefully after item $i"
        exit 143
    fi
done
echo "all items done"
""")

run("""
chmod +x worker.sh
!bash worker.sh
echo "worker exit status: $?"
""")

Output

$ chmod +x worker.sh
$ bash worker.sh
processing item 1
processing item 2
received TERM at item 2
stopping gracefully after item 2
cleaned state.json (rc=143)
[exit status 143]
$ echo "worker exit status: $?"
worker exit status: 143

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 graceful_worker_session.py

The handler set STOP=1 and printed the item at which TERM arrived. The main loop finished item 2, saw STOP, printed stopping gracefully after item 2, and exited 143. The EXIT trap then removed state.json and reported rc=143. The pattern:

  • Set trap early, before the loop that handles items, so a signal between items is handled too.
  • Keep the handler short. Set a flag and let the loop act on it. Bash runs a trap only between commands: while a foreground command such as sleep 60 runs, the handler waits for it to finish (the Bash manual, Signals). Start a long wait as sleep 60 & wait $! instead, because wait returns at once when a trapped signal arrives.
  • Exit with the conventional status. exit 130 for INT and exit 143 for TERM give a caller that reads $? the same number as a death by that signal, while scripts that forget report 0 and look as though they finished the job. The script still ended normally, though, and a calling Bash script can tell: it treats a SIGINT as fatal only when the command it was waiting for was killed by it (the Bash manual, Signals). To end as if killed, reset the trap and send the signal again: trap - INT; kill -INT $$.

Ignoring SIGHUP keeps a long job alive when its terminal closes

When a terminal closes, its shell receives SIGHUP, and an interactive Bash sends it on to every job before it exits (the Bash manual, Signals). A job that must survive can be started with nohup, taken off the job list with disown, or can ignore the signal itself with trap '' HUP, since an empty action means ignore. The processes-jobs-and-signals module of this track covers running scripts in the background.

Checkpoint

Check: Handling INT and TERM, and exiting 130 or 143

  1. Question 1 of 2 A worker sets trap "echo got TERM" TERM and runs sleep 60. kill -TERM arrives after 1 second. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: sleep keeps running; when it ends 59 seconds later, the handler prints "got TERM" and the script goes on

    For a wait that must react to signals, run it as sleep 60 & wait $!: wait returns at once, with a status above 128, when a trapped signal arrives, and the handler runs right after.

  2. Question 2 of 2 A script with no INT or TERM trap is killed by Ctrl-C. What exit status does the caller see?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 130, which is 128 + 2 for SIGINT

    The convention is 128 + signal number: 130 for INT (Ctrl-C), 143 for TERM, 139 for SEGV, 141 for PIPE. Catching INT and TERM lets the handler clean up and still return the right code.

ERR, RETURN and DEBUG, and the subshell reset

Three Bash traps are not signals but shell events. The A name that trap accepts in place of a signal for an event of the shell itself: EXIT when the shell exits, ERR when a command fails outside the spots where set -e looks away, RETURN when a function or sourced file returns, DEBUG before each command. names are:

  • ERR: runs when a command fails, except in the spots where set -e looks away, whether or not set -e is on. Not inherited into a function unless set -E (errtrace) is on.
  • RETURN: runs when a function or a sourced file returns to its caller. Not inherited unless set -T (functrace).
  • DEBUG: runs before every simple command (and before a few compound ones, such as for and case). Also inherited only with set -T.
set -eE: errtrace lets the ERR trap reach into functions Shell · err_errtrace.sh
#!/bin/bash
# errtrace (set -E) makes the ERR trap inherit into functions and $() substitutions.
# Read-only snippet; a real script opens with this at the top.
set -eE
trap 'echo "ERR at line $LINENO: $BASH_COMMAND"' ERR
ERR with set -E, the subshell reset, trap -p and dash's refusal of -p Python · err_debug_session.py
from session import run, write

# ERR runs when a command fails, outside the spots where set -e looks away. By default the ERR trap does not reach
# into a function; set -E (errtrace) makes it inherited. set -T (functrace) does the same for RETURN and DEBUG.
# A subshell resets every trap the shell has set, EXIT included (ignored signals stay ignored), so the parent's EXIT
# trap does not run when a subshell ends, and a trap set inside ( ... ) fires only when that subshell ends.
# trap -p lists what is set. POSIX.1-2024 added trap -p; dash 0.5.13.5 rejects it, and because trap is a special
# builtin the dash script ends with status 2.
write("err_demo.sh", """#!/bin/bash
set -eE
trap 'echo "ERR at line $LINENO: $BASH_COMMAND" >&2' ERR

echo "about to call a function"
greet() { echo "hello"; false; }
greet
echo "unreachable"
""")

write("subshell_reset.sh", """#!/bin/bash
trap 'echo "parent EXIT fired"' EXIT

( echo "inside a subshell that sets no trap of its own" )
(
    echo "inside subshell before setting its trap"
    trap 'echo "subshell EXIT fired"' EXIT
    echo "inside subshell after setting its trap"
)
echo "subshell ended; parent still running"
""")

run("""
chmod +x err_demo.sh subshell_reset.sh
!bash err_demo.sh
bash subshell_reset.sh
# trap -p lists what the shell has set:
bash -c "trap 'echo hi' EXIT; trap -p EXIT"
# POSIX.1-2024 added trap -p; dash 0.5.13.5 rejects it, and because trap is a special builtin the dash script ends:
!dash -c "trap 'echo hi' EXIT; trap -p EXIT; echo after"
""")

Output

$ chmod +x err_demo.sh subshell_reset.sh
$ bash err_demo.sh
about to call a function
hello
ERR at line 6: false
[exit status 1]
$ bash subshell_reset.sh
inside a subshell that sets no trap of its own
inside subshell before setting its trap
inside subshell after setting its trap
subshell EXIT fired
subshell ended; parent still running
parent EXIT fired
# trap -p lists what the shell has set:
$ bash -c "trap 'echo hi' EXIT; trap -p EXIT"
trap -- 'echo hi' EXIT
hi
# POSIX.1-2024 added trap -p; dash 0.5.13.5 rejects it, and because trap is a special builtin the dash script ends:
$ dash -c "trap 'echo hi' EXIT; trap -p EXIT; echo after"
dash: 1: trap: Illegal option -p
hi
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 err_debug_session.py

In the session, err_demo.sh has set -eE, so the ERR trap reached into greet and reported “ERR at line 6: false” the moment false failed. subshell_reset.sh showed the second rule, which the Bash manual states for every subshell: traps the shell has set are reset, EXIT included, while ignored signals (trap '' SIG) stay ignored. The first ( … ) sets no trap and ended without running the parent’s EXIT trap; the second set its own, which fired when it ended; the parent’s fired once, when the script ended.

trap -p lists what is currently set, printing a line per signal that reads like the trap command used to set it; POSIX.1-2024 added -p to the standard (its change history lists it for Issue 8), and Bash supports it. dash 0.5.13.5 does not yet, and because trap is a POSIX special builtin, a syntax error in it ends the shell: dash -c "trap -p EXIT; echo after" printed the usage error and then the EXIT handler’s output (“hi”), status 2, with no “after”. For a listing that works in every POSIX shell, dash included, run trap with no arguments.

Expecting an ERR trap to fire inside every function

Wrong: set -e; trap … ERR; f() { false; }; f and waiting for the trap. Right: set -eE; trap … ERR; f() { false; }; f. The Bash manual says ERR is not inherited by functions unless errtrace is on; the same applies to DEBUG and RETURN under functrace.

Checkpoint

Check: ERR, RETURN, DEBUG and the subshell reset

  1. Question 1 of 2 A script has set -e and trap "echo ERR at $LINENO" ERR. It calls a function f that runs false on its first line. Does the ERR trap print, and what fixes it if not?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It does not: the ERR trap is not inherited into functions by default; add set -E (or combine as set -eE)

    set -T (functrace) is the same switch for RETURN and DEBUG; a script that wants all three can open with set -eET.

  2. Question 2 of 2 A script runs trap "echo parent EXIT" EXIT, then ( trap "echo child EXIT" EXIT; echo inside ). What prints?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: inside, child EXIT, then parent EXIT when the script ends

    Ignored signals, such as trap "" HUP, do carry into subshells; it is the only exception.

Practise: predicting what a trap does

Each question changes one detail of the sessions: a signal name, a missing set -E, a double-quoted action, a long command running when TERM arrives. Predict the result first; the explanations name the run behind each answer:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What is the one way out of a shell that an EXIT trap does not run?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: A SIGKILL (kill -9 or the Linux OOM killer)

    In Bash, an EXIT trap covers four of the five ways out that the lesson's figure shows; SIGKILL is the one that no trap can cover.

  2. Question 2 of 10 Which exit status does the shell report for a script killed by an untrapped SIGTERM?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: 143 (128 + 15)

    Pair the convention with a trap: handle TERM, finish the current item, and call exit 143 so the caller sees the same code it would see without the trap.

  3. Question 3 of 10 A script has trap cleanup EXIT and trap on_term TERM. External kill -TERM arrives. In what order do the two traps run?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: on_term runs; then, if it calls exit 143 or the shell exits for any other reason, cleanup runs on the way out

    A handler that returns without calling exit leaves the shell running; the EXIT trap waits for the shell's actual exit, which may be after the loop finishes.

  4. Question 4 of 10 Why does a cleanup action start with rc=$?, and what breaks when it does not?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: The next command in the trap resets $?; saving it first lets the trap log, return or branch on the status that caused the exit

    The trap's own commands do not change the status the script ends with; rc=$? is about knowing that status inside the trap, to log it or to branch on it.

  5. Question 5 of 10 A long-running job must survive a terminal closing (SIGHUP). Which trap arranges that?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: trap "" HUP (an empty action, which means ignore)

    The two usual forms are nohup command & disown and trap "" HUP; the processes-jobs-and-signals module covers both.

  6. Question 6 of 10 A script runs trap cleanup EXIT, then later wants to clear the trap. Which command removes it?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: trap - EXIT

    trap '' EXIT, and in Bash also trap EXIT with no action, leave nothing to run as well, but trap - EXIT is the reset POSIX defines. trap '' HUP is different: it makes a signal ignored.

  7. Question 7 of 10 ShellCheck reports SC2064 for which of these trap lines?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: trap "rm -rf -- $tmp" EXIT

    The two safe forms are a single-quoted action and a function registered by name; the function keeps a longer cleanup readable.

  8. Question 8 of 10 A script wants ERR to fire inside its functions and RETURN to log every function return. Which lines are needed?

    Choose every answer that is right.

    Show the answer: Question 8 of 10

    Answer:

    • set -E
    • set -T

    A script that needs both can combine them with the strict options: set -eETuo pipefail is errexit, errtrace, functrace, nounset and pipefail.

  9. Question 9 of 10 A script runs trap "" HUP, then starts ( sleep 2; echo survived ) & and sends SIGHUP to that background subshell. What happens?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The subshell ignores HUP too, so it finishes and prints survived

    Use trap "" HUP when a long-running job must survive terminal closes; the ignore propagates into every subshell the job starts.

  10. Question 10 of 10 A script ends with exit 7, and its EXIT trap runs commands whose last status is 0. What status does the caller see?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: 7, the status set by exit before the trap runs

    To change the exit status from inside the trap, call exit with a new number explicitly; otherwise the status set before the trap stays.

Interview questions

Warm-up (fresher to 2–5 years): what does an EXIT trap, trap 'commands' EXIT, guarantee, and what does it not?

It guarantees that Bash runs commands when the shell exits: at the end of the script, at an exit, when set -e stops it, and, in Bash 5.3, when an INT, TERM or HUP with no trap of its own ends it. It does not cover SIGKILL (kill -9, the Linux OOM killer), which signal(7) says cannot be caught, and it does not promise that commands finish: a power cut can stop them half-way. It is also not portable: dash runs no EXIT trap when a signal ends the script, so a script for sh traps INT and TERM as well. Write the cleanup so that it is safe to run twice and touches only what the script created.

Two questions follow, both about cleanup that went wrong in production: a lock file that outlived its service, and a trap that removed a folder it was never meant to touch. Think through your first step for each before you open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A service writes /var/run/import.lock when it starts and removes it in an EXIT trap. After the OOM killer ended it, the lock stayed, and every later start refused with "lock held". No trap can run after SIGKILL, so design a lock that recovers without one, and say what each design can still get wrong.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A build script sets workdir=./out at the top. A helper function declares local workdir, fills it with mktemp -d and sets trap 'rm -rf -- "$workdir"' EXIT, single-quoted as ShellCheck asks. When the script ended, ./out and its results were gone and the temporary folder was still there. Explain what ran, and fix it.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does trap 'action' SIGNAL do?

    It registers action to run when SIGNAL, or one of the shell events EXIT, ERR, RETURN and DEBUG, reaches the shell. The action is one shell word.

  2. Why should a trap action be in single quotes?

    So its variables expand when the trap runs, not when it is set; double quotes freeze the values of the moment, the slip ShellCheck names SC2064.

  3. When does an EXIT trap run in Bash, and when can it not?

    However the shell ends: the last line, exit, a set -e stop, or an INT or TERM. It never runs after SIGKILL, which no process can catch, and dash runs it on none of these signals.

  4. Which status should a script exit with after trapping INT or TERM?

    The handler does not exit on its own, so write exit 130 for INT and exit 143 for TERM, which is 128 plus the signal number, and the caller sees the right code.

  5. How much work belongs in a signal handler?

    Very little: set a flag and let the main loop act on it. Bash runs a trap only between commands, so a foreground sleep 60 delays the handler; start a long wait as sleep 60 & wait $!, because wait returns at once when a trapped signal arrives.

  6. Why does an ERR trap not fire inside functions by default?

    Functions do not inherit it unless set -E is on. RETURN and DEBUG traps need set -T for the same reason.

  7. What happens to traps in a subshell, and how do you list them?

    A subshell resets every trap except ignored signals, so a trap set inside ( ... ) fires when that subshell ends. trap -p lists the current traps in Bash and POSIX.1-2024, but dash 0.5.13.5 rejects -p.

Key takeaways

  • trap 'action' SIGNAL registers action to run when SIGNAL (or EXIT, ERR, RETURN, DEBUG) reaches the shell. The action is one shell word; single-quote it so variables expand at trap time, as ShellCheck’s SC2064 names.
  • In Bash, an EXIT trap runs however the shell ends (the last line, exit, a set -e stop, an INT or TERM) but never after a SIGKILL, which signal(7) says cannot be caught; dash runs it on none of the signals.
  • A trapped INT or TERM runs the handler but does not exit on its own: write exit 130 for INT and exit 143 for TERM so the caller sees the right code, and keep the handler’s work small (set a flag; let the main loop finish its item, and wait for long commands with wait).
  • ERR runs when a command fails outside the spots where set -e looks away; it is not inherited by functions unless set -E is on. RETURN and DEBUG need set -T for the same reason.
  • A subshell resets every trap but ignored signals, so a trap set inside ( … ) fires only when the subshell ends; trap -p lists the current traps in Bash and in POSIX.1-2024, while dash 0.5.13.5 rejects -p, and because trap is a special builtin that error ends the dash script.

Terms in this lesson

trap
A Bash builtin that registers code to run on a signal or a shell event: EXIT when the shell exits, ERR when a command's failure would end a set -e script, RETURN when a function returns, DEBUG before every simple command. trap action signal …; the action is one shell word.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.