1. Overview

When we run our scripts, we don’t expect them to finish without any errors. We foresee this and write our code to accommodate these unexpected scenarios. And on error, we might want to show a message to the user and exit from the script. In addition to that, we’ll print enough logs to debug the issue later.

For this tutorial, we’ll check the different ways we can raise an error and exit from the script.

2. Using the set -e Option

Bash has this built-in set command to set different options in the shell. One of the many options is errexit. Merely setting this option helps us exit the script when any of the commands return a non-zero status.

Let’s look at an example:

$ cat set.sh 
#!/bin/bash
set -e
ls
ls -l nofolder
ls -l
$ ./set.sh 
set.sh
ls: cannot access 'nofolder': No such file or directory
$

In the above script, we’ve used the set -e command to enable the errexit option. From the results, we can see the first ls command is executed. For the second one, it has printed the error that the folder is not accessible since nofolder doesn’t exist. And for the last ls command, we don’t see the result. That means the script has exited on encountering the error in the previous command.

As we can see, using one line of code, we were able to achieve what we required.

Even though this is simpler to implement, there are some caveats to note. Let’s look at some of these cases.

2.1. Working With Pipelines

Firstly, the return status of a pipeline is the exit status of the last command. Hence, if we’ve multiple commands in a pipeline and all fail but the last one, then it is treated as a success. Consequently, the script doesn’t stop execution.

Let’s take a look at this with an example:

$ cat set.sh 
#!/bin/bash
set -e
ls
ls | ls -l nofolder | ls
ls -l
$ ./set.sh
set.sh
ls: cannot access 'nofolder': No such file or directory
-rwxrwxr-x 1 bluelake bluelake 306 Mar 28 13:00 set.sh
$

As seen above, we’ve got a failing ls command in the middle of a pipeline. Even though it prints an error the script doesn’t stop there. From the results, we can see it executed till the last ls command.

Luckily, for this problem, there is a simple solution.

Next, let’s see how we can solve this:

$ cat set.sh 
#!/bin/bash
set -e
set -o pipefail
ls
ls | ls -l nofolder | ls
ls -l
$ ./set.sh
set.sh
ls: cannot access 'nofolder': No such file or directory
$

As shown above, we’ve added another option pipefail using the set command. Hence, the exit status of the pipeline will be the exit status of the last command to exit with a non-zero status. As a result, the script has stopped execution when it encountered an error in the pipeline.

2.2. Other Problems

In addition to this, there are a few more scenarios to be careful about while using this set command.

Firstly, the commands wrapped in an if or a while statement don’t trigger an exit condition.

Secondly, the commands in an && or an || operator are immune to this errexit option.

Let’s look at an example for these cases:

$ cat set.sh 
#!/bin/bash
set -e
echo "And and Or operator:"
ls && ls -l nofolder || ls
echo "While loop:"
while ls -l nofolder; do
    echo "Folder exists"
    break
done
echo "If condition:"
if ls -l nofolder; then
    echo "Folder exists"
fi
ls -l
$ ./set.sh 
And and Or operator:
set.sh
ls: cannot access 'nofolder': No such file or directory
set.sh
While loop:
ls: cannot access 'nofolder': No such file or directory
If condition:
ls: cannot access 'nofolder': No such file or directory
total 24
-rwxrwxr-x 1 bluelake bluelake 192 Mar 28 17:34 set.sh
$

We can see from above that none of the errors caused the script to exit. We’ve put the echo command throughout the script to understand the flow.

Lastly, there are cases where it triggers this error condition unwantedly.

Let’s look at this script below:

$ cat t.sh 
#!/bin/bash
set -e
i=0
echo "Incrementing"
let i++
ls
$ ./t.sh 
Incrementing
$

As shown in the above script, we can see that the last ls command didn’t execute. The let command incremented the value of variable i to one and returned it. Then, it is interpreted as the exit code from the last command and triggered the errexit condition. Hence, the script stopped execution.

Similarly, there are more scenarios to watch out for. For a complete list, we can check the Bash man page.

3. Using the if-else Block

This is a straightforward way to check for the result of a command and raise an exception.

Let’s check an example:

$ cat if.sh 
#!/bin/bash

if ! ls -l nofolder; then
    echo "Folder doesn't exist"
    exit 1
fi
ls -l
$ ./if.sh 
ls: cannot access 'nofolder': No such file or directory
Folder doesn't exist
$

As shown above, we’ve wrapped the command that may cause an error in an if clause. Then we check the exit code for that command, log an error, and exit from the script.

To make this better, we may move the handling part to a separate function. Then we can call that function passing the error string when we encounter any error.

Conversely, if we’ve got a small script and don’t have a reusable scenario like the above, we may simplify this a bit like below:

$ cat if.sh 
#!/bin/bash
ls -l nofolder || exit 1
ls -l
$ ./if.sh 
ls: cannot access 'nofolder': No such file or directory

Here, we’ve used the OR operator to identify the error and exit from the script.

3.1. Using PIPESTATUS

While using the if statement to raise an exception, we’ve got a problem when we’ve commands in a pipeline. We need to know when any command in the pipeline fails. For this, we can use the PIPESTATUS array.

The PIPESTATUS array will have the exit code for each command in the pipeline at their specific index. Then we can check the exit code for each command and raise an error if needed.

Let’s check this with an example:

$ cat pipe.sh 
#!/bin/bash
ls | grep nofolder | wc -l
result=`echo ${PIPESTATUS[@]} | grep -E '^[0 ]+$'`
if [ "$result" = "" ]; then
    echo "Command failed"
    exit 1
fi
ls
$ ./pipe.sh 
0
Command failed

In the above script, the grep command in the pipeline fails. Then, we get the exit code in the PIPESTATUS variable. We then run the contents of this variable through another grep command. There, we check if we’ve any character other than zero or space in it. In case of an error, the PIPESTATUS variable will have an exit code other than zero. Hence, the result variable will be empty. Finally, we can check for this condition, print the error, and exit the script.

4. Using the trap Command

The trap is a shell built-in command, which is used to run commands on receiving specific signals. Of the different signals, ERR is the signal of our interest. This is emitted when any command is exited with a non-zero status code.

Let’s see how we can use the trap command:

$ cat trap.sh 
#!/bin/bash
function handler() {
    echo "Error"
    exit 1
}
trap handler ERR
ls -l nofolder
ls
$ ./trap.sh
ls: cannot access 'nofolder': No such file or directory
Error
$

In the above snippet, we’ve declared a function handler with an exit command. Then, we bound the handler function to ERR signal using the trap command. After that, we ran the ls command which fails. This then raises an ERR signal, which invokes the handler function. Finally, from the handler function, we print an error and exit from the script.

This is similar to the set -e command we looked at earlier. The same caveats apply here as well. We can check the man page for a complete list. But one advantage here is, we get to do some housekeeping through the handler before we exit from the script.

However, the set command also gives us an opportunity to run a handler before we exit. Let’s take a look at it next.

4.1. Combine set and trap

We can combine the set and the trap command to run a handler.

Let’s look at an example:

$ cat combine.sh
#!/bin/bash
set -e
trap "echo 'Trapped'" ERR
ls -l nofolder
ls -l
$ ./combine.sh
ls: cannot access 'nofolder': No such file or directory
Trapped
$

Here, it’ll log a message when there is an error using the trap handler. Since we’ve set the errexit option, it’ll automatically exit the script when an error occurs. We can see that we haven’t written the exit command in the trap handler.

5. Conclusion

In this tutorial, we’ve seen different ways we can raise an error and exit from the script. As demonstrated, not all options are complete on their own, but we might have to combine these as required for our use cases.