Shell scripting error handling

2026 Mar 23

If you already know about bash’s set -euo pipefail and can explain every part individually, well, you’re way ahead of me.

I’ve known about setting -euo pipefail, but I can’t seem to recall what each part does. Furthermore, I can’t say what the equivalents are in other languages like Ruby, JavaScript/zx, Java/jbang, or even zsh!

So let’s just review the basics, first …

set -euo pipefail, and friends

Let’s take a look at what mohanpedala wrote about it, who basically copied what aaron maxwell wrote, though mohan’s gist discussion also leads to an interesting Cloudflare post, so ups and downs.

set -e exits when any command completes with a non-zero exit status code. I guess I wonder if bash itself exits with a non-zero code (and if it’s the same code). This is pretty strict, but reasonable. You can temporarily disable it with set +e ... set -e.

set -x prints to the terminal the commands that are being run.

set -u exits when any undefined variable is referenced (instead of inserting like an empty string).

set -o pipefail raises any error in a pipeline to be the pipeline’s exit code (and not just the final command in the pipeline).

Traps can run functions on exit, like defers in Go.

function cleanup { ... }
trap cleanup EXIT

Okay, but what if I’m not using bash ?

zsh

zsh is the new bash, at least as far as macOS is concerned. The manual describes …

$ man zshoptions
...
ERR_EXIT (-e, ksh: -e)
      If a command has a non-zero exit status, execute the ZERR trap,
      if set, and exit.  This is disabled while running initialization
      scripts.
...

It does look like there’s a PIPE_FAIL option, but I can’t tell if it has to be capitalized, or if the underscore is required, or what

PIPE_FAIL
      By default, when a pipeline exits the exit status recorded by
      the shell and returned by the shell variable $? reflects that of
      the rightmost element of a pipeline.  If this option is set, the
      exit status instead reflects the status of the rightmost element
      of the pipeline that was non-zero, or zero if all elements
      exited with zero status.

set -o pipefail does seem to work the same way.

set -u seems to be the same

UNSET (+u, ksh: +u) <K> <S> <Z>
      Treat unset parameters as if they were empty when substituting,
      and as if they were zero when reading their values in arithmetic
      expansion and arithmetic commands.  Otherwise they are treated
      as an error.

set -x seems to dump a ton of debug information? The docs seem to say it’s pretty similar to bash:

...(bash)
  -x      After expanding each simple command, for command, case
          command, select command, or arithmetic for command,
          display the expanded value of PS4, followed by the
          command and its expanded arguments or associated word
          list.
...                      
...(zsh)
XTRACE (-x, ksh: -x)
      Print commands and their arguments as they are executed.  The
      output is preceded by the value of $PS4, formatted as described
      in the section EXPANSION OF PROMPT SEQUENCES in zshmisc(1).
...

Hmm. If I have a simple script like

% cat -n script 
     1	set -x
     2	uname

then it’ll print like

% . ./script 
+./script:2> uname
Darwin
+update_terminal_cwd:5> local url_path=''                                       
+update_terminal_cwd:10> local i ch hexch LC_CTYPE=C LC_COLLATE=C LC_ALL='' LANG=''
+update_terminal_cwd:11> i = 1
...like 300 more lines of update_terminal_cwd

So it does seem to work as described, but it’s not obvious where all this update_terminal_cwd stuff is coming from. Interesting.

It seems like it’s a macOS thing that can be worked around, so that’s a wrap, I guess. I can set -euxo pipefail to my heart’s content in zsh as well.