4 min read

set -euo pipefail Saved My Deploy Script. Here's How It Works

Last week my deploy script told me everything was fine. It wasn't.

The build had failed about forty lines in, but bash just kept going. It ran the rest of the script against a half-built artifact, uploaded the mess, printed "Deploy complete!" and exited 0. My CI pipeline turned green. Production was broken for an hour before anyone noticed.

That's the day I stopped trusting bash to fail on its own.

Why Bash Doesn't Fail (Unless You Ask It To)

Here's the thing nobody tells you when you start writing shell scripts: bash doesn't stop when a command fails. It just keeps going. Every command's exit status gets checked by you — or it doesn't get checked at all.

Most of us don't check. I sure didn't.

#!/bin/bash
cd /var/www/my-app
npm run build
rsync -avz dist/ user@server:/var/www/my-app/
echo "Deploy complete!"

If npm run build fails, that script happily rsyncs the old dist folder and prints success. The exit code is whatever echo returns, which is always 0. Your script "works" — right up until it doesn't.

The Three Flags That Fix It

There's a one-line fix most of my scripts were missing. It's called strict mode, and it looks like this:

#!/bin/bash
set -euo pipefail

Three flags, one line. Changes everything about how your script behaves.

set -e: Stop on Error

set -e makes the script exit immediately when any command returns a non-zero status. That deploy script above? With -e, a failed build kills the script right there. No rsync, no fake success message, no broken production.

Sounds perfect. It's not, and I'll get to why in a second.

set -u: Catch Unset Variables

set -u treats a reference to an unset variable as an error. Without it, a typo like ${ENVIRNOMENT} silently expands to an empty string. Your script runs, does nothing, and you spend an hour wondering why.

With -u, bash screams at you the moment you touch a variable that doesn't exist. It's annoying at first. It's worth it.

set -o pipefail: The Pipeline Trap

This one's sneaky. set -e doesn't catch failures inside a pipeline — only the exit code of the last command matters.

set -e
npm run build | tee build.log   # build fails, but tee exits 0

The build fails, tee succeeds, and -e sees a zero. pipefail fixes that by making the pipeline return the rightmost non-zero exit code. Now a failure anywhere in the pipe fails the whole thing.

Where Strict Mode Bites Back

Look, I'm not going to pretend strict mode is all sunshine. It has sharp edges, and you'll hit them in the first week.

The big one: set -e doesn't play nice with commands you expect to fail. Checking if a directory exists is fine — -e is disabled inside if conditions. But a bare grep that finds nothing exits 1, and -e kills your script. The fix is || true when you genuinely don't care:

grep "TODO" src/ || true

Or wrap a block in set +e and set -e when you want it to survive. Ugly, but it works.

What Every Script I Write Starts With Now

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

The IFS line is a bonus — it stops word-splitting on spaces and tabs, which is a whole other class of bug. Filenames with spaces stop breaking your for loops.

That's it. Two lines. They've saved me more times than I can count, and the deploy script that broke production last week? It's got them now. I keep a copy of my go-to script template in Snippet Ark so I never retype it — and so the next script I write starts safe instead of starting from scratch.

Strict mode won't make your scripts perfect. But it'll make them honest. They'll fail loudly when they fail, instead of smiling at you while they break things.