1. Introduction

Errors are a normal part of Linux administration and Bash scripting. They indicate special and potentially important system states. Despite this, we could want to skip processing some errors.

In this tutorial, we discuss errors and how to ignore them in Bash. In particular, we define what errors are and categorize them. Next, we see how they are identified in general and in Bash. After that, we show some ways to ignore and suppress errors. Finally, some remarks demonstrate specific cases.

We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It is POSIX-compliant and should work in any such environment.

2. Errors

Although we often encounter errors, they are an exceptional state. For this reason, another name for an error is an exception. Exceptions represent conditions of software that did not achieve its desired goal.

Because of the differing circumstances, we can roughly sort errors by origin.

2.1. Hardware Errors

The lowest level of any software product is the hardware on top of which it runs. While electrical and mechanical issues are outside the scope of this article, the fact that they commonly have a software expression is important.

Consider the following scenarios:

  • a failing hard disk generates a read or write error
  • physical memory is corrupt and causes applications to crash for any number of reasons
  • a processor overheats, forcing a reboot

Importantly, hardware errors can often be unavoidable via software. In part, the same applies to the following error category as well.

2.2. Operating System Errors

Since the operating system (OS) is the base of all applications, an OS error can easily affect them. In a broader sense, we consider the operating system to be the combination of kernel, device drivers, an API, user interface, and file system. Consequently, any errors in these components are OS errors.

In fact, operating systems are themselves software products. That said, same as with hardware, their errors are hard to ignore.

For example, a corrupt OS file can prevent applications from even running. Another case might involve missing or misconfigured drivers, which cause errors during device access. Due to such circumstances, applications can throw errors, despite not causing them in the first place.

2.3. Application Errors

Of course, even if the OS functions without issues, an application can misuse or misconfigure it.

This is often due to bad user input. For instance, trying to access non-existent file results in an application exception. Other times the OS itself can be corrupt, preventing otherwise valid operations.

On the other hand, we have mishandled user input. Instead of the application reporting an issue, the OS might prevent a process from performing an illegal action. Imagine a scenario in which a user attempts to delete somebody else’s file. Filesystem permissions will be enforced by the OS, regardless of any checks in the application.

In addition, applications can misbehave intentionally but also as a result of unexpected environments. That is, applications themselves can have bad or incomplete code.

2.4. Programming Errors

Whereas the errors discussed so far mainly were outside our control, access to source code can change that. In particular, we can implement checks and boundaries during development that prevent issues during runtime.

Of course, development presents its own challenges:

  • syntax errors, where the programming language rules are broken
  • logic errors, such as infinite loops, bad boolean conditions, and incorrect flow
  • runtime errors like memory misallocation

How preventable, avoidable and ignorable these errors depend on their specifics, as well as the programming language.

To deal with an error, we must first know that it happened at all. How? That depends on the origin.

3. Error Codes

Problem diagnosis is not straightforward. OS errors can report themselves during startup. Hardware errors might even be indicated by bad smells.

Applications, on the other hand, have status or exit codes. Exit codes allow a user of the application to know its final status. Importantly, the widely accepted status number for no errors is 0.

Even though errors often come with textual clarification, that is not always the case. Here are some common POSIX error codes with their descriptions:

  • EBADF=9, Bad file descriptor
  • EFAULT=14, Bad address
  • EIO=5, I/O error

Applications can omit the description and just return the error (code).

In essence, developers can consider and plan for different conditions and situations. However, they can rarely encompass all possible scenarios. Importantly, the OS can terminate a rogue process, which also results in an exit code.

Errors in Bash can also result in exit codes, as we’ll see below.

4. Bash Errors

Before running, Bash script lines have to be interpreted. Therefore, each command construct has a return code.

Based on this return code and other conditions, Bash determines whether a given command construct failed.

Here’s an example with cd (change directory):

$ cd DoesNotExist
-bash: cd: DoesNotExist: No such file or directory
$ echo $?
1

If command exits with a non-zero status, it failed. This usually has no direct consequences, but can be detrimental when errors are not caught. To avoid hidden issues, we use set with the -e flag, with which failing command constructs result in immediate exit with a non-zero code. Since set -e is often recommended in production environments, abrupt exits might seem inevitable. There are, however, ways to avoid them, which we will discuss below.

5. Bash Error Handling

We can imagine the OS and hardware as a skeleton, which holds applications. If the skeleton is compromised, these applications may not work correctly. They don’t have the necessary tools to remedy the situation. Applications can only check for issues. Hence we won’t deal with those scenarios, but just their consequences.

As developers, we are mainly interested in problems we can deal with. We handle execution status codes of software we call while controlling our own exit codes and programming errors. Take the following script as an example:

read input
if [[ "$input" == 'hello' ]]; then
exit 0
fi
exit 1

We use read to get input from the user. After that, the input is compared to a predefined string. As long as the script runs its course, we return our own status codes. However, if the script is killed prematurely, this fact will be indicated by status 137 under POSIX.

If our script was called greet. sh, here is how one could check the exit code after piping the “hello” input:

$ echo hello | bash greet.sh
$ echo $?
0

In Bash, the $? variable stores the status code of the last command. Indeed, this is valid for any application call in our script.

Since zero indicates success as an exit code in Bash, we can chain calls with logical operators:

$ echo bye | bash greet.sh && echo Success. || echo Failure.
Failure.

After piping “bye” to our script, it returns a non-zero (fail) error code. This means the && (and) construct is skipped, while the || (or) is triggered.

6. Ignoring Errors in Bash

Most importantly, the whole line has an exit code of zero (success), regardless of the path in it:

$ echo bye | bash greet.sh && echo Success. || echo Failure.
Failure.
$ echo $?
0

Indeed, this comes in handy when set -e is in play, as it prevents abrupt script termination for a particular command. Note that the method only works when the commands after && and || return success. We use echo, but an alternative option for silent success is the : (null utility).

Another way of achieving the same is with the exclamation point syntax:

set -e
! echo bye | bash greet.sh
echo Success.

Executing the script above does not force an exit, despite the error in a set -e environment.

Yet another option is piping to any command that executes successfully:

set -e
echo bye | bash greet.sh | echo $?
echo Success.

Although echo $? shows 1, we still succeed, because it returns 0.

Finally, we can also turn off the -e setting before running our command, reenabling it after:

set -e
# code here
set +e
echo bye | bash greet.sh
set -e
echo Success.

In addition, we can also suppress any stdout and stderr error messages that the failing command may print:

$ badcommand
-bash: badcommand: command not found
$ badcommand >/dev/null 2>&1

Using a combination of exit code processing and ignoring the output, we can achieve a silently failing command.

All methods discussed can be applied to a whole block of code by surrounding it with curly brackets:

set -e
! {
echo First command.
badcommand
echo Third command.
}
echo Success.

Next, we’ll see some exceptional cases.

7. Remarks

In most scenarios, we can predict with good certainty when errors are possible. Some commands, however, have inherent exceptions built-in.

7.1. Heredoc

One construct is using read with a heredoc:

$ read -d '' var << EOI
Line one.
Line two.
EOI
$ echo $?
1

Error code 1? What’s the error? It turns out read returns a non-zero error code when it does not encounter a delimiter. In this case, the delimiter (-d) is an empty string (), i.e., NUL. However, the heredoc does not end in a NUL, so it returns non-zero (fail).

7.2. Pipefail

Another gotcha that’s worth mentioning is the pipefail option of set. When enabled, the return code of a pipeline is the value of the last command to exit with a non-zero status. This means any failures along the pipe cause immediate termination of the pipe chain and command:

set -e
badcommand | echo
# script does not exit
set -o pipefail
badcommand | echo
# script exits

Initially, piping a non-zero exit code to successful command results in overall success. After setting pipefail, the same line generates an error and forces the script to exit.

7.3. Undefined Variables

Yet another set flag is -u. Once we set -u, it forces any reference to an undefined variable to result in an error:

set -e
set -u
echo $undefined
# script exits

Along with the usual ways, there is a special construct that allows us to bypass this error: ${undefined:-SOME_VALUE}. When we dereference a variable like this, it either returns the variable value or, if undefined, it returns SOME_VALUE. Consequently, we do not use an undefined value and hence to not generate an error with set -u.

7.4. Compiling and Interpreting

In terms of programming syntax errors, if one is encountered when processing a source code file, compilers and interpreters return error codes themselves:

$ touch empty.c
$ gcc empty.c
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/10/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x20): undefined reference to `main'
collect2: error: ld returned 1 exit status
$ echo $?
1
$ echo [ > bad.sh
$ bash bad.sh
empty: line 1: [: missing `]'
$ echo $?
2

7.5. Traps

While not specifically designed to ignore errors, we can use trap to avoid abrupt exits. In a way, traps prevent or add to the default handling of an error. Without getting too much into traps, here’s a simple example script:

set -e
trap 'echo "Inside trap"; echo "Line $LINENO."' ERR
echo 'Before bad command.'
badcommand
echo 'Unreachable.'

As the output of this script shows, we can execute multiple commands before a script exits or errors out:

$ bash script.sh
Before bad command.
./script.sh: line 5: badcommand: command not found
Inside trap.
Line 5.

To circumvent any of the errors in this section, we can utilize the methods we discussed in the previous one.

8. Summary

In this article, we discussed errors in Bash.

In conclusion, Bash has a standard, but diverse error mechanism, which we can employ to handle and ignore errors.