Why does `trap` passthough zero instead of the signal the process was killed with?

Consider the following:


trap 'echo $?' INT
kill -INT $$

Output: 0

Here I would expect 130 for my system. Of course, if I do a Ctrl + C then I get 130.

The same thing happens for any other signal like HUP or TERM. I found this behavior surprising because if you have a trap set up to capture many signals then there’s no way to exit with the correct error code for what signal called your handler:


exit_abrupt() {
    echo "Encountered an error, cleaning up..." >&2
    # *clean up*

    exit "$exit_code" # This will return 0!

trap exit_abrupt HUP INT TERM
kill -HUP $$

I’ve tested Bash, Dash, and ZSH, all of which exhibit this behavior. Is this how it’s supposed to work across shells? Is it documented by POSIX (can someone point to the documentation)? How else can I know the correct exit code that works across shells?

The best POSIX documentation I’ve found reads:

The value of "$?" after the trap action completes shall be the value it had before trap was invoked.

which to me sounds like it should be passing though the signal in all cases and yet it’s not so I must be missing something…

Asked By: Elliot Killick


$? contains the exit status of the last command that was run and waited for. You’ll find that in:

$ bash -c 'trap "echo $?" INT; sleep 10; exit'

130 was reported because both sleep and bash received SIGINT upon ^C, bash ran the handler after sleep returned and printed the exit status of that sleep command.


$ bash -c 'trap "echo $?" INT; kill -s INT "$$"; exit'

You get the exit status of the kill command which was the last command bash ran before the SIGINT handler was invoked.

$ bash -c 'trap "echo $?" INT; (trap "" INT; sleep 3; exit 123); exit'

sleep ignores SIGINT, you’ll see that upon pressing Ctrl+C, you still have to wait for the subshell running sleep to return at which point the handler prints the exit status of that subshell (123).

If you want to install the same handler for several signals, you can pass the signal to the handler:

handler() {
  local signal="$1"
  echo "I got $signal signal"
for signal in INT HUP TERM QUIT; do
  trap "handler $signal" "$signal"

BTW, you’ll find that:

bash -c '(trap "" INT; sleep 3; exit 123); exit'

Exits with a 123 exit status even when you try to interrupt it with Ctrl+c, and that’s actually as designed in bash. More on that at When typing ctrl-c in a terminal, why isn't the foreground job terminated until it completes?

Answered By: Stéphane Chazelas

Here’s my final solution for combining an EXIT trap with a fatal signal trap (whether received by keystroke or a signal sent to the shell process) while always passing through the correct exit status in case anyone wants:


set -e

# https://unix.stackexchange.com/questions/752570/why-does-trap-passthough-zero-instead-of-the-signal-the-process-was-killed-wit
handle_exit() {

    if [ "$exit_code" != 0 ] || [ "$signal" ]; then
        echo "nProgram was exited abruptly!" >&2

    # Or do clean up here...

    if [ "$exit_code" != 0 ]; then
        trap -- - EXIT
        exit "$exit_code"
    elif [ "$signal" ]; then
        trap -- - "$signal"
        kill -s "$signal" -- $$

# POSIX sh doesn't include signals in its EXIT trap so list them ourselves
# SIG prefixes removed for POSIX sh compatibility
for signal in HUP INT TERM; do
    # shellcheck disable=SC2064
    trap "handle_exit $signal" "$signal"
trap handle_exit EXIT

# Your code starts here...

The trap -- - EXIT is necessary because if you trigger a literal INT (Ctrl + C) then the INT trap will rightfully go off before the EXIT trap. In this particular case (because of the literal Ctrl + C), $exit_code will also be set to 130. So, we remove remove the EXIT trap from the if branch to avoid triggering the handler twice. Similarly, trap -- - "$signal" avoids recursively calling the handler forever until stack overflow occurs.

Across all the shells (even Posh), it works like a charm.

Answered By: Elliot Killick
Categories: Answers Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.