To make a zsh pipeline report a failure from any stage, enable the PIPE_FAIL option before running it. In Terminal on Mac, enter setopt PIPE_FAIL; in a script, put that line before the pipeline. The pipeline then returns the exit status of its rightmost failed command, or zero if every command succeeds. Capture $? immediately after the pipeline if later commands must not replace that result. The zsh options manual documents this behavior.

Why a failed pipeline can look successful

By default, zsh gives a pipeline the exit status of its final command. So if an earlier command fails but the final command succeeds, the overall pipeline can appear successful. For example, the final cat below can succeed even when grep finds no matching line:

printf '%s\n' 'ready' | grep 'missing' | cat
print -r -- $?

The zsh manual defines a pipeline as commands joined by | and says its value is normally the value of the last command. A zero status means success; a nonzero status means the command or pipeline reported failure. The zsh pipeline documentation describes the default rule.

Enable PIPE_FAIL for the pipeline

In a Terminal session, set the option before running the command:

setopt PIPE_FAIL
printf '%s\n' 'ready' | grep 'missing' | cat
print -r -- $?

The final status is now nonzero because grep failed, even though cat succeeded. PIPE_FAIL uses the rightmost nonzero status in the pipeline. This keeps the failure meaningful while still returning the actual status code selected by zsh. To turn the behavior off again in the same session, run unsetopt PIPE_FAIL.

For a script, place the option near the top, after the interpreter line and before any affected pipeline:

#!/bin/zsh
setopt PIPE_FAIL

printf '%s\n' 'ready' | grep 'missing' | cat
pipeline_status=$?

if (( pipeline_status != 0 )); then
  print -u2 -- "Pipeline failed with status $pipeline_status"
  exit "$pipeline_status"
fi

The explicit status assignment matters: $? means the status of the last command, so a later print, variable-related command or conditional can change what it reports. Saving the value immediately lets the script report it or exit with it after other work. This example returns a failing status to the program that launched the script; it does not stop individual pipeline commands from running as soon as an earlier stage fails.

Use pipestatus when you need each stage’s result

PIPE_FAIL summarizes the pipeline. If you also need to identify which command returned which code, zsh provides the special array pipestatus, containing the status of each command in the last pipeline. Read it right away, because another pipeline replaces those values. The zsh parameter reference documents the array.

printf '%s\n' 'ready' | grep 'missing' | cat
print -r -- "Stage statuses: ${pipestatus[*]}"

For this three-command example, the output shows a zero for printf, a nonzero value for grep, and a zero for cat. The exact nonzero code depends on the command. If you need both the overall result and per-stage results, use PIPE_FAIL and capture the overall $? immediately; inspect pipestatus immediately after a separate pipeline run when diagnosing stages, or design the script to record the array first and compute the desired aggregate from that saved data. Avoid inserting another command or pipeline between the pipeline and the status you intend to read.

Keep the setting in the right shell

To apply the option to later commands in an interactive zsh session, run setopt PIPE_FAIL in that session. To enable it for each new zsh session, add the line to ~/.zshrc, then open a new Terminal window or source the file with source ~/.zshrc. Apple’s Terminal guide identifies zsh as the default shell, while noting that settings or administrators can select a different shell. Confirm the active shell before relying on zsh-specific syntax. Apple’s Terminal guide explains how Terminal chooses the shell.

A script that starts with #!/bin/zsh requests zsh directly, regardless of the interactive shell configured for Terminal. If the script is instead run by another shell, setopt and pipestatus are not portable substitutes for that shell’s own pipeline controls. Keep the interpreter and syntax aligned, then preserve the pipeline result before doing follow-up work.

Sources