The Bash Trap Trap

Dirk Avery
5 min readFeb 27, 2019

--

Traps are a cool way to implement error handling in Bash scripts. However, if you think traps work the same as exception handling in Python, Go, or Java, watch out for a big trapping trap!

Trap Basics

A Bash trap allows you to execute a command when a specific situation occurs. The command can be a regular command, executing one or more commands in a subshell, or a function call. The situations that can be trapped include errors (ERR), script exit (EXIT), and the interrupt (SIGINT), terminate (SIGTERM), and kill (KILL) signals.

For example, this Bash script will catch an error and allow you to perform some handling and even ignore the error if you want.

#!/bin/bashtrap 'catch' ERRcatch() {
echo "An error has occurred but we're going to eat it!!"
}
echo "Before bad command"
badcommand
echo "After bad command"

This script will output the following:

Before bad command
./script.sh: line 10: badcommand: command not found
An error has occurred but we're going to eat it!!
After bad command

Execution is able to reach the “After bad command” line because we didn’t exit as part of our error handling. If you add an exit 1 to the end of the catch() function, execution would not reach anything after the trap occurs. The great thing about traps is that you have the option to handle the error how you want.

If you want to get slightly fancier, you can easily capture the error code and line number where the error occurred.

#!/bin/bashtrap 'catch $? $LINENO' ERRcatch() {
echo "Error $1 occurred on $2"
}
echo "Before bad command"
badcommand
echo "After bad command"

Now the script will give us a bit more information:

Before bad command
./script.sh: line 10: badcommand: command not found
Error 127 occurred on 10
After bad command

The Problem: Functions

Simply put, all errors occurring after the trap call should trigger the trap. Right? In most programming languages, an exception within a called function is still an exception and will send the execution thread to the encompassing exception handling.

However, that is not how Bash works. Consider the following Bash script.

#!/bin/bashtrap 'catch $? $LINENO' ERRcatch() {
echo "Error $1 occurred on $2"
}
simple() {
badcommand
echo "Hi from simple()!"

}
simple

From this script you might expect the following output:

./script.sh: line 10: badcommand: command not found
Error 127 occurred on 10
Hi from simple()!

But that is not what you’ll get! The catch function is never called. This is the actual output:

./script.sh: line 10: badcommand: command not found
Hi from simple()!

What the heck happened to the trap? 😡

(Full disclosure: The rage face above is my actual face when I was trying to debug this problem.)

For purposes of error handling, think of functions as subshells. The only thing that the outer scope cares about is the function’s return value.

This behavior is similar to how Bash treats the final statement of a script as the return status for the entire script.

To control the return status of a script, you use exit <status> and to control the return status of a function, you use return <status>. Otherwise, the return status of a function or script is the return status of the last executed statement.

In the previous script, if the error would have occurred as the last statement of the function, the return value of the entire function would have indicated an error and been caught by the trap. In this script, the lines in simple() are inverted so that the error occurs last.

#!/bin/bashtrap 'catch $? $LINENO' ERRcatch() {
echo "Error $1 occurred on $2"
}
simple() {
echo "Hi from simple()!"
badcommand

}
simple
echo "After simple call"

Bash considers simple()'s return status to be the bad command’s return status, 127.

The updated script now outputs the following:

Hi from simple()!
./notrap4.sh: line 11: badcommand: command not found
Error 127 occurred on 14
After simple call

A Robust Solution 👍

Bash’s assumptions based on the last executed statement are not a good basis for robust error handling. Although we cannot make Bash error handle like Python, Go, or Java, we can make it much better than what we’ve seen so far.

Returning to “The Problem” script above, here is what I want in an error handling solution:

  1. Catch errors in functions, even when they’re not the last statement
  2. Perform error handling after the error but before exiting (e.g., copy logs from a remote server)
  3. Not clutter up a script with traps everywhere
  4. Not check the return status of every statement in a function and decide whether to return or not

Here’s what I’ve come up with to satisfy all of these requirements. The solution has three parts: a) set exit-on-error mode (set -e), b) trap the exit signal instead of the error, and c) make sure we ended up in catch based on an error. This is the re-written script from above (the script where the error in the function was not the function’s last statement):

#!/bin/bashset -e
trap 'catch $? $LINENO' EXIT
catch() {
echo "catching!"
if [ "$1" != "0" ]; then
# error handling goes here
echo "Error $1 occurred on $2"
fi
}
simple() {
badcommand
echo "Hi from simple()!"
}
simple
echo "After simple call"

The output from the script is as follows:

./script.sh: line 13: badcommand: command not found
catching!
Error 127 occurred on 12

We don’t see the “Hi from simple()!” or “After simple call” messages — the script exited before execution reached those lines.

A handy feature is that the return status from the script is the same as the initial error code.

$ echo $?
127

If we comment out the badcommand line (# badcommand), the output of the script is the following:

Hi from simple()!
After simple call
catching!

Notice that we end up in catch() at the end when the script exits even though there was no error. Mind the catch!

One big potential drawback of this solution is that you cannot ignore errors that make it to the trap. You can do things before exiting, but the script is going to exit.

Other Options

Besides the robust and strict solution above, there are a few other less charming solutions.

One option is to trap errors at the global level but within a function check the return code of every statement and return immediately on an error.

Or, we could keep track of a return code in a variable, based on the cumulative success and failure of all the statements, and then return that value. This way all statements could execute even if an error occurs, but the function would be caught regardless of where in the function the error occurred.

Another option is to include traps within your functions. If you include enough traps, you can make sure that every statement’s error’s are trapped.

There are probably other good solutions. Let me know!

--

--

Dirk Avery

Cloud engineer, AI buff, patent attorney, fan of cronuts. AWS Certified Solutions Architect — Professional. Go, Python, automation. https://www.hashicorp.com