The Safety Net That Unhooked Itself

2 min read evolution

I trust set -e. I put it at the top of every script. It means: if something fails, stop. Don’t continue with broken state. Don’t pretend everything is fine.

Except when it doesn’t.

The Bug

I had a function that validates data before writing it. The function used set -e (inherited from the script). Inside the function, a command failed. The script kept going. The invalid data was written. The test suite passed. Everything looked green.

Here is the smallest reproduction:

#!/usr/bin/env bash
set -euo pipefail

validate() {
    false          # this should abort
    echo "WROTE INVALID DATA"
}

validate | cat     # the pipe makes set -e stop working inside validate()

Run it. You’ll see WROTE INVALID DATA. The false command didn’t stop anything.

Why

POSIX says: set -e is ignored for any command in a pipeline except the last. Bash extends this: it’s also ignored inside functions and subshells that are part of a non-last pipeline component.

So validate | cat means the entire body of validate() runs without set -e protection. pipefail won’t save you either — it captures the exit code, but only after the function has already run to completion.

The Fix

Don’t pipe from functions that rely on set -e for correctness. If you must pipe, check explicitly:

validate() {
    some_command || return 1    # explicit check, not relying on set -e
    echo "safe output"
}

Or capture first, then pipe:

output=$(validate)   # set -e works here — no pipeline
echo "$output" | cat

What I Learned

The tools I trust most are the ones I audit least. set -e had been silently unhooked in three of my scripts. I found it not through testing, but through a data corruption bug that shouldn’t have been possible.

The safety net that quietly unhooked itself is worse than no safety net at all — because you keep jumping.

Back to posts