Bash Error Guide: 'set -e' Unexpected Exit — Fix Scripts That Die Silently
Fix Bash 'set -e' unexpected exits: understand which commands trigger errexit, guard expected failures with || true, and trap ERR to see the real cause.
- #bash
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
With set -e (errexit) enabled, Bash exits the moment a command returns non-zero — and scripts often die at a line the author thought was harmless, with no error message of their own:
$ ./deploy.sh
$ echo $?
1
No traceback, no explanation — just a non-zero exit and a job marked failed. set -e is doing exactly what it was told: the last command returned non-zero, so the shell exited. The confusion comes from which commands count, and from the many exceptions to the rule. Understanding those exceptions turns “it exits and I don’t know why” into a precise diagnosis.
Symptoms
- The script stops partway with no output and a non-zero exit status.
- Removing
set -e“fixes” it — but really just hides a command that was failing. - A
grepthat finds no match, adiffthat finds a difference, or a[ ]test that is false silently ends the script. - The failure disappears when the same command runs at an interactive prompt (where
set -eis off).
Common Root Causes
- A command legitimately returns non-zero and isn’t guarded —
grep pattern fileexits 1 when there’s no match; underset -ethat ends the script. let,(( )), orexprevaluating to 0 — arithmetic that results in0returns exit status 1 (e.g.(( count++ ))whencountstarts at 0).- A failing command in a pipeline with
set -o pipefailon — any stage failing propagates. - A function returning the status of its last command unexpectedly.
set -enot applying where you expect — it’s ignored for commands inif/whileconditions,&&/||lists, and!-negated commands, which masks some failures and surprises with others.- A subshell or sourced script changing errexit behavior.
Diagnostic Workflow
Add an ERR trap to print the failing line and command — the single most useful change:
set -Eeuo pipefail
trap 'echo "ERR: exit $? at line $LINENO: $BASH_COMMAND" >&2' ERR
set -E makes the trap inherit into functions and subshells. Run with -x to see the last command before exit:
bash -x ./deploy.sh 2>&1 | tail -20
Check the exit status of a suspect command explicitly:
grep pattern file; echo "grep exit: $?"
Example Root Cause Analysis
A release script exited silently after adding set -euo pipefail:
#!/usr/bin/env bash
set -euo pipefail
count=$(grep -c "ERROR" app.log) # exits here when there are zero errors
echo "found $count errors"
send_report "$count"
On a clean log it printed nothing and exited 1. The ERR trap revealed the culprit:
ERR: exit 1 at line 4: count=$(grep -c "ERROR" app.log)
grep -c prints 0 and returns exit status 1 when there are no matches. Under set -e, that non-zero status ended the script before echo ran. The fix guards the expected “no match” case:
count=$(grep -c "ERROR" app.log || true) # tolerate zero matches
echo "found $count errors"
send_report "$count"
|| true swallows grep’s benign non-zero, so a clean log now reports found 0 errors instead of aborting.
Prevention Best Practices
- Install an ERR trap —
trap 'echo "failed: $BASH_COMMAND (line $LINENO)" >&2' ERRturns silent exits into pinpointed messages. - Guard commands whose non-zero is expected —
cmd || true, orif ! cmd; then ...; fito handle the failure deliberately. - Know the
set -eexceptions — it does not trigger on commands inif/whiletests,&&/||chains, or after!; structure code accordingly. - Be careful with
(( ))andlet— use(( count++ )) || trueorcount=$((count+1))(assignment, which is always status 0). - Use
set -Eeuo pipefailtogether, and understandpipefailmakes any pipeline stage’s failure count. - Test the failure paths, not just the happy path, so an expected non-zero doesn’t surprise you in prod.
Quick Command Reference
set -Eeuo pipefail # strict mode with trap inheritance
trap 'echo "ERR $? @ $LINENO: $BASH_COMMAND" >&2' ERR # pinpoint the exit
count=$(grep -c X f || true) # tolerate expected non-zero
if ! cmd; then handle_failure; fi # deliberate failure handling
n=$((n+1)) # assignment: always exit 0
bash -x ./script.sh 2>&1 | tail # see the last command run
Related Guides
- Bash Error Guide: ‘unbound variable’ — the
set -uhalf of strict mode and its surprises. - Bash Error Guide: ‘integer expression expected’ — numeric failures that can end a strict-mode script.
- Bash & Python Error Guide: ‘command not found’ — a missing binary returns 127 and, under set -e, exits immediately.
Conclusion
A silent set -e exit isn’t a bug in Bash — it’s a command returning non-zero that you didn’t expect to matter, like grep with no match or (( count++ )) evaluating to 0. Add an ERR trap to name the failing command and line, guard expected non-zero exits with || true or explicit if handling, and learn the errexit exceptions. Strict mode then catches real failures loudly instead of ending your script in silence.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.