Standard meaning of exit codes
POSIX defines that a process exits with a status. The _Exit and exit functions pass that status to the parent. Most tools follow the convention that zero indicates success and nonzero indicates failure or an error condition.
PowerShell follows a similar pattern. Normal termination sets the exit code to zero. An uncaught throw sets it to one. The exit statement lets a script set any specific value before stopping.
- Zero always means success under the common convention.
- Nonzero values are interpreted by the calling script or scheduler.
- Some tools use specific numbers such as 130 for interruption by Ctrl+C.
Why pipelines hide failures
A pipeline runs several commands at once. The shell waits for every stage but normally returns only the exit status of the final command. Earlier stages can fail completely and the pipeline still reports success because the last program finished without error.
This behavior appears in everyday commands such as counting lines from a nonexistent directory or searching logs that produce no matches. The downstream command exits zero and the overall status looks clean even though an upstream step failed.
- false | true reports zero because true succeeds.
- ls on a missing path followed by wc still exits zero.
- The shell discards intermediate statuses unless told otherwise.
Enabling pipefail for safer scripts
Bash, ksh, and zsh provide the pipefail option. When enabled, the pipeline status becomes the rightmost nonzero exit code or zero if every command succeeded. Adding set -o pipefail near the top of a script changes the default behavior without rewriting every pipeline.
The option works together with set -e. A failed pipeline now aborts the script as intended. Without -e the script continues but the captured status is already the correct one.
- Place set -o pipefail early in the script header.
- Combine with set -euo pipefail for stricter execution.
- Test the change by deliberately inserting a failing command at the start of a pipeline.
Reading every stage with PIPESTATUS
When pipefail alone is not enough, inspect the status of each stage. Bash stores the exit codes of every command in a pipeline inside the PIPESTATUS array. The array is overwritten by the next command, so copy it immediately after the pipeline finishes.
A safe pattern stores the array into a new variable on the following line. Later code can then examine which stage produced the nonzero code and decide whether that stage should be treated as fatal.
- echo "${PIPESTATUS[@]}" shows every code right after the pipeline.
- st=("${PIPESTATUS[@]}") preserves the values for later use.
- Zsh uses the lowercase pipestatus array with one-based indexing.
Practical patterns and cautions
Some filters legitimately exit nonzero when they find nothing. Grep returns one for no matches. In those cases wrap the command with || true inside a subshell or block so pipefail does not treat the expected result as failure.
Early-exit consumers such as head or grep -q can send SIGPIPE to upstream producers. The resulting 141 status is often harmless but must be recognized when reviewing PIPESTATUS output. Capture output first or tolerate the specific code when the consumer is expected to stop reading.
- Use { grep pattern || true; } to allow expected nonzero results.
- Capture output with a variable when early exit is likely.
- Document any tolerated nonzero codes in comments near the pipeline.
Cross-platform and long-term considerations
POSIX shells do not standardize pipefail, so scripts that must run under plain sh need alternative approaches such as temporary files or explicit status checks after each command. PowerShell scripts can use $LASTEXITCODE or the automatic $? variable after each statement.
Semantic exit code schemes used by some tools separate user errors from software errors. Scripts that call such tools should map the documented ranges rather than treating every nonzero value identically.
- Check for pipefail support with sh -c 'set -o pipefail' before relying on it.
- Store exit codes early when mixing tools that use custom ranges.
- Review the calling environment when moving scripts between shells or CI systems.