Shell scripting patterns: errors and failure

In my opinion, error handling is the area where shell script clearest shows its age. The idea that the shell will by default ignore that commands signal an error state (i.e. a non-zero exit code) seems very strange when viewed through the lens of modern programming theory. Thus, all scripts should start with this instruction, that tells the shell to care about exit codes:

set -e

Now, the following code will no longer result in tears when /destination does not exist:

process < /source/file > /destination/file
rm /source/file

To explicitly ignore the exit code of a command, use:

process < /source/file > /no/such/file || true

... or if you care, but don't want to just bail, you can:

may_explode || result=$?

Just remember that $result will be empty rather than 0 if all goes well, so you cannot do [ $result -gt 0 ].

In rare cases, it may genuinely be that you have a section of instructions that you want to execute even if the previous fail. If so, use a subshell:

(
    set +e
    ...
)

The set operation will only be active in the subshell.

It is quite possible to achieve something akin to modern exception handling in bash shells. Consider the following:

(
    do_this || exit 1
    do_that || exit 2
) || {
    fail=$?
    case $fail in
    1) handle_case_1 ;;
    2) handle_case_2 ;;
    *) exit $fail ;;
    esac
}

Of course, do_this, do_that may already return decent exit codes such that you don't need the '|| exit' bit. More generally, now that set -e is turned on, you need a mental model of execution flow, much like any other programming language. Just remember the law: any context (be it subshell, function or block) has an exit code/return value of the first statement executed that returns a non-zero exit code/return value. Somewhat simplified, as soon as a statement returns non-zero, execution will abandon successive scopes until someone "handles" the non-zero state. The three main ways to "handle" the non-zero state are pipe "|",  command substitution "$()", or "||". The background control operator "&" also does not propagate the exit code (it can later be retreived with wait).

It may be tempting to use exit to make life simpler inside functions, but you should resist. exit will abort execution of the current shell, but parentheses and pipes create subshells, so a function cannot know whether it will abort the master shell process or just a subshell. Use only return statements in functions and save exit for top-level code. exit should only be needed in top-level code to tell the caller that parameters or input were invalid.