Desk Clarity

Everyday document & browser help

Bash Pipeline Hides a Failure: Check Pipefail and Each Stage’s Status

By ON Lab · Sources checked November 8, 2026

A Bash pipeline normally reports the last command’s status. If an earlier command fails but a logger or reader succeeds, set -e alone may not stop the script. set -o pipefail changes the aggregate status. It does not prevent later pipeline commands from starting or replace deliberate handling of expected failures.

Test a harmless pipeline before changing CI

Atlassian documents successful Bitbucket build steps despite a piped command failing. Inspect the boundary locally without running a build or contacting CI:

bash -c 'set -e; false | cat; printf "continued\n"'
bash -c 'set -e -o pipefail; false | cat; printf "continued\n"'

Our Bash 3.2.57 fixture printed continued and exited 0 for the first command. The second printed nothing and exited 1. The producer and consumer were unchanged; their aggregate pipeline status changed. Record the actual shell used by a CI wrapper before applying this result. We tested Bash, not every program named sh.

The rightmost nonzero status wins

bash -c 'set -o pipefail; (exit 7) | (exit 3) | cat; printf "%s\n" "$?"'

This printed 3 locally. It was neither the first failing status, 7, nor the final successful status, 0. One aggregate result cannot identify every component failure.

Capture individual statuses immediately

set -o pipefail
false | cat
parts=("${PIPESTATUS[@]}")
printf '%s\n' "${parts[*]}"

Run this small example as a Bash script without errexit. It printed 1 0. Capture PIPESTATUS immediately after the pipeline; another command updates it. Even a diagnostic print before capture changes what you are inspecting. If errexit would stop execution before capture, choose an explicit handling structure instead.

Handle an expected failure as a decision

set -e -o pipefail
if false | cat; then
    printf 'pipeline accepted\n'
else
    result=$?
    printf 'pipeline failed with status %s\n' "$result"
    # Decide whether to stop, recover, or return a documented result.
fi

The corresponding local case entered the failure branch and printed status 1. Conditional context matters: errexit has documented exceptions. Adding || true explicitly suppresses failure; our test with that suffix continued despite the failing producer. It is not a universal repair.

An early reader can make the producer fail

In our yes | head -n 1 test, pipefail yielded status 141. The consumer had obtained its one line; the upstream process encountered a closed pipe. This negative control shows why a nonzero aggregate does not always mean that no useful output was produced.

Do not copy a special-case status rule into unrelated commands. Establish whether early termination is expected for that producer/consumer pair and check their outputs and statuses. A different command contract may fit better than globally disabling pipefail.

Keep application success and variable scope separate

A process can return 0 while an application rejects a request. The curl HTTP-status check addresses that separate boundary. Pipefail aggregates supplied statuses; it does not reinterpret HTTP responses.

Likewise, a variable set in a piped loop may not survive outside it. See the pipeline counter guide. Exit aggregation and variable ownership require different checks.

The free Bash documentation and Atlassian article already explain the option. This guide adds seven controlled cases, per-stage capture, explicit failure handling and an early-reader counterexample. No paid CI service or infrastructure change is required for those local tests.

Method: current GNU Bash documentation, a documented Bitbucket support symptom and seven synthetic Bash 3.2.57/macOS cases. No live CI job, remote deployment, production logger or other shell was tested. Status 141 is an observation from the local yes/head fixture.

Sources