Bash Error Handling with Trap


Yesterday I ended up writing an impromptu guide to Bash error handling on a PR, so I decided to polish it a bit and turn it into an actual post.

The goal: whenever our release script encounters an error, send a notification to a Slack channel. We won’t look into the latter part in this post, as it was handled by some Ruby code using the slack-notifier gem. Instead we’ll look into what was necessary to make this work in Bash.

Exiting On Errors

The first step is to add the -e (or -o errexit) option to the script, which will exit at the first error. This is contrary to Bash’s default behavior of continuing with the next command:

set -e

There are some other options one should consider adding at this point:

set -Eeuo pipefail

While debugging it may also be useful to add x (-o xtrace) to the options, which will print all expanded commands to stdout before executing them:

bash -x script.sh

Trapping Errors

Traps in Bash are used for executing a command or series of commands upon catching a signal. For example if we want to print a message when the user hits Ctrl-C, we can do it in the following way:

#/bin/bash

trap "{ echo 'Bye!' ; exit 0; }" SIGINT
while true ; do sleep 1 ; done

Let’s see this in action:

$ bash -x infinite.sh
+ trap '{ echo '\''Bye!'\'' ; exit 0; }' SIGINT
+ true
+ sleep 1
+ true
+ sleep 1
^C++ echo 'Bye!'
Bye!
++ exit 0
$ echo $?
0

This script is running an infinite loop, but when the user sends a SIGINT signal it will print a message and exit with a successful error code.

This same mechanism can also be used to perform cleanup tasks when a script terminates:

#/bin/bash

trap "rm test" EXIT

echo "Hello, reader!\n" > test
cat test

This will create a file named test, output its content and then remove it on exit. Let’s see this in action and verify that the file has indeed been removed:

$ bash -x cleanup.sh
+ trap 'rm test' EXIT
+ echo 'Hello, reader!\n'
+ cat test
Hello, reader!\n
+ rm test
$ file test
test: cannot open `test' (No such file or directory)

There’s also a special signal named ERR, which will be triggered every time a command exits with a non-zero status. This is exactly what we need to make our Slack notifier work:

#!/bin/bash
function notify {
  echo "Something went wrong!"
}

trap notify ERR

nonexisting_command

As you can see trap also supports calling functions, which we use here to invoke notify whenever an error occurs:

$ bash -x err.sh
+ trap notify ERR
+ nonexisting_command
err.sh: line 8: nonexisting_command: command not found
++ notify
++ echo 'Something went wrong!'
Something went wrong!

Generating the Message

For the purpose of our Slack notifier, we didn’t just want to know that something went wrong, bug also what exactly the error was. Once again Bash had us covered by providing the caller builtin, which will output information about execution frames.

Let’s update the last script to make use of this functionality:

#!/bin/bash
function notify {
  echo "Something went wrong!"
  echo "$(caller): ${BASH_COMMAND}"
}

trap notify ERR

nonexisting_command

Running this will generate the following error message:

$ bash err.sh
err.sh: line 9: nonexisting_command: command not found
Something went wrong!
9 err.sh: nonexisting_command

Here “9” is the line number where the error occurred, “err.sh” is the script that triggered it and “nonexisting_command” is the command that caused the error (provided by the $BASH_COMMAND variable). Alternatively we could also have used the $LINENO variable:

- echo "$(caller): ${BASH_COMMAND}"
+ echo "Error on line ${LINENO}: ${BASH_COMMAND}"

This generates the following output: “Error on line 4: nonexisting_command”.

Using all of the described features, we end up with the following script:

set -Eeuo pipefail

notify () {
  FAILED_COMMAND="$(caller): ${BASH_COMMAND}" \
    # perform notification here
}

trap notify ERR

# actual release commands

Comments powered by Talkyard.