Linux / shell
set -e did not stop the script — check which exit status Bash actually saw
Written and reviewed by Sahil Srivastav
pipeline_status=0What this error actually means
The header shows the output of the first reproduction below, not a runtime exception. A failed command does not necessarily make its enclosing pipeline fail. By default, Bash reports the status of the last pipeline command. If a producer fails and cat successfully consumes the resulting empty stream, the pipeline can return zero and set -e has no failure status to act on.
The pipefail option changes pipeline status to the rightmost nonzero status when a pipeline component fails. It does not turn errexit into exception handling. Bash deliberately suppresses errexit in several tested contexts, including commands whose status is used by if or parts of AND/OR lists. A function called in such a context can continue after a failure inside its body.
Command substitution is another boundary. In ordinary non-POSIX Bash, errexit is normally cleared in the subshell used for $(...). inherit_errexit can change that on supporting versions, but the status of the outer command still matters. Logging an expansion with echo may return success even when the expansion’s command failed.
Causes, most common first
- 1The pipeline reports only its final command’s status. A compressor or consumer can successfully finish even after the producer fails. set -e sees the pipeline’s aggregate status, not an unconditional instruction to abort whenever any process anywhere returns nonzero.
- 2The function is invoked where failure is being tested. Putting a function in if, or on the left of an OR list, can disable the expected errexit behaviour inside that function. Critical commands need explicit checks and returns when the caller intentionally handles failure.
- 3Command substitution hides or replaces failure. The substitution may continue after a failed command and exit with a later successful status. Alternatively, the surrounding command may succeed regardless. Assigning output and checking the assignment’s status avoids that second source of masking.
- 4An expected nonzero status is mistaken for an incident. grep returns nonzero for no match, and an upstream producer can receive SIGPIPE when a downstream head deliberately stops early. Enabling pipefail may surface these conditions. Decide which are acceptable instead of appending || true everywhere.
When you see it
- A backup pipeline creates an empty output and the script prints success
- A function behaves differently when called directly versus inside if
- A failed command inside $(...) is followed by another command that succeeds
- Adding pipefail reveals failures previously hidden by a successful consumer
How to diagnose it
Step 1
Reproduce the default pipeline result
This isolated Bash command uses no application data. false fails, cat finishes successfully, and the script prints pipeline_status=0. It demonstrates why errexit alone does not protect a producer-consumer pipeline.
bash -c 'set -e; false | cat; printf "pipeline_status=%s\n" "$?"'Step 2
Compare the same pipeline with pipefail
Run this as a standalone diagnostic command. Bash exits nonzero at the pipeline, so the marker is not printed. The surrounding terminal can then inspect its exit status; do not embed this experiment in a production release script.
bash -c 'set -eo pipefail; false | cat; printf "unexpected continuation\n"'Step 3
Capture component statuses immediately
PIPESTATUS is a Bash array overwritten by subsequent commands. Disable errexit in this isolated experiment so the reporting command runs. In real scripts, capture the array immediately if per-stage status is needed.
bash -c 'set +e; false | cat; printf "stages: %s\n" "${PIPESTATUS[*]}"'Step 4
Check the interpreter and the calling context
Inspect the shebang, Bash version, function call site and any command substitutions around the failure. /bin/sh may be a different shell. A script launched with sh script.sh does not gain Bash semantics from a Bash shebang.
bash --version
head -n 1 ./backup.shThe fix
Use a declared Bash interpreter and enable pipefail for pipelines where every stage must succeed. Add explicit conditionals at consequential boundaries such as publishing a backup or marking a deployment complete. Errexit remains useful as a backstop, but the business invariant should be visible in control flow.
The example compresses an existing export.sql into a temporary file beside its destination. It publishes only when the pipeline succeeds, and the EXIT trap removes an incomplete temporary artifact. The backup directory must exist. A real exporter should also signal an incomplete export with a nonzero exit status.
For a function whose caller tests success, check each critical command inside the function and return failure explicitly. For command substitution, separate declaration from assignment and test the assignment. Avoid local result=$(command) when the declaration builtin’s status can mask the command’s failure.
Handle expected nonzero results deliberately. If no search match is an acceptable outcome, distinguish it from a read error. If early consumer exit is intentional, design that pipeline’s status policy explicitly. A blanket || true removes the evidence needed to tell acceptable absence from lost output.
#!/bin/bash
set -euo pipefail
backup_tmp=$(mktemp ./backups/.export.XXXXXX)
trap 'rm -f -- "$backup_tmp"' EXIT
if cat -- ./export.sql | gzip >"$backup_tmp"; then
mv -- "$backup_tmp" ./backups/export.sql.gz
else
printf 'Export pipeline failed; backup not published\n' >&2
exit 1
fi
trap - EXITHow to stop it coming back
- Inject a producer failure and verify that no successful artifact or completion marker is published.
- Review functions used in conditional contexts for explicit error propagation.
- Document intentional nonzero statuses and keep their handling local to the command that produces them.
FAQ
Does set -euo pipefail make every script safe?
No. These options address selected failure modes and have context-dependent behaviour. They do not validate output completeness, quote arguments or roll back external side effects. Explicit checks still belong around operations that determine success.
Should I enable inherit_errexit?
It can help on Bash versions that support it, but it does not eliminate conditional-context exceptions or an outer command masking the substitution’s status. Prefer simple substitutions whose result is checked directly, and verify the deployed Bash version.
Why did pipefail break a pipeline using head?
head can exit after enough input, closing the pipe while the producer still writes. That producer may receive SIGPIPE. Decide whether that early stop is expected for this operation rather than treating every newly visible nonzero result alike.
Related
Other errors engineers hit next to this one
- QueuePool limit of size 5 overflow 10 reached, connection timed out
- DetachedInstanceError: instance is not bound to a Session
- RuntimeError: Event loop is closed
- Task was destroyed but it is pending!
- Executing <Handle ...> took 2.418 seconds (blocked event loop)
- SettingWithCopyWarning: A value is trying to be set on a copy of a slice
- celery.exceptions.WorkerLostError: Worker exited prematurely
- requests.exceptions.ReadTimeout: HTTPSConnectionPool read timed out