Skip to content
Shell & TerminalFix Published Updated 4 min readViews unavailable

Fixing 'Broken Pipe' Errors in Shell Scripts and Pipelines

A command mid-pipeline dies with 'Broken pipe,' often only when piped into head — a specific, well-defined signal, not a random failure.

A “Broken pipe” error appearing in a shell pipeline — often specifically when piping into head, grep -m, or another command that can exit before consuming all its input — is a well-defined, specific signal behavior, not a random or unexplainable failure.

Step 1: understand what’s actually happening

yes | head -3

yes produces output indefinitely; head -3 reads exactly 3 lines and exits immediately afterward. Once head closes its end of the pipe, yes receives SIGPIPE — a signal specifically sent to a process attempting to write to a pipe whose reading end has already closed — and typically terminates as a result, hence “broken pipe.”

Step 2: recognize this as normal, expected pipeline behavior

This isn’t a bug in either yes or head — it’s the intended, standard mechanism for stopping an upstream command once a downstream command no longer needs more input, letting head -3 cleanly stop an infinitely-producing command like yes without needing any special coordination between the two.

Step 3: check whether the “broken pipe” error is actually harmless

some-long-running-command | head -20
echo "Exit status: ${PIPESTATUS[0]}"

If the downstream command (head, in this example) got everything it needed before the upstream command was terminated by SIGPIPE, the overall pipeline typically achieved its intended result despite the visible error message — worth confirming before treating it as an actual failure needing a fix.

Step 4: suppress a specific, known-harmless SIGPIPE error in a script

some-command 2>/dev/null | head -5

If a specific pipeline’s SIGPIPE-caused error message is expected and harmless, redirecting standard error away is a reasonable, deliberate way to suppress the specific noise without affecting the pipeline’s actual functional result.

Step 5: handle SIGPIPE explicitly within a script that shouldn’t die from it

trap '' PIPE

A script that specifically shouldn’t terminate on SIGPIPE (uncommon, but occasionally needed for a specific control-flow reason) can trap and ignore the signal explicitly — though this should be used deliberately and narrowly, not as a default habit, since SIGPIPE’s default behavior exists for good reason in most pipelines.

Step 6: check for a genuinely different problem masquerading as a broken pipe

some-command | another-command
# if another-command crashed or was killed for an
# UNRELATED reason, the upstream command will also
# see a broken pipe as a downstream consequence

If a downstream command in a pipeline crashes or is killed for a completely separate reason (an actual bug, an out-of-memory kill), the upstream command will also receive SIGPIPE as a direct consequence — worth checking the downstream command’s own exit status and logs specifically, rather than assuming the upstream command’s broken-pipe error is the primary problem.

Step 7: check exit statuses of every stage in a pipeline, not just the last one

some-command | another-command
echo "${PIPESTATUS[@]}"

$? alone only reflects the last command in a pipeline’s exit status — ${PIPESTATUS[@]} (Bash-specific) shows the exit status of every stage, letting you distinguish “the upstream command was cleanly stopped by SIGPIPE as expected” from “something further down the pipeline actually failed.”

Why understanding SIGPIPE as a deliberate mechanism, not an error, changes how you troubleshoot it

Treating every “broken pipe” message as a bug to eliminate leads to suppressing genuinely useful signals or adding unnecessary complexity to scripts — recognizing SIGPIPE as the specific, intentional mechanism that lets a pipeline’s downstream command cleanly stop an upstream producer is what lets you correctly distinguish expected, harmless pipeline termination from an actual problem elsewhere in the pipeline.

Recognizing the specific exit-code signature

SIGPIPE is signal number 13 on essentially every Unix-like system, and a process killed by it reports an exit status of 128 plus the signal number — 141 — rather than a small, ordinary-looking exit code. Seeing exactly 141 in ${PIPESTATUS[@]} output is a strong, specific confirmation that a stage was terminated by SIGPIPE specifically, rather than failing for some unrelated reason that happened to also produce a nonzero exit status.

Why a Python script upstream in a pipeline behaves differently

python3 -c "
for i in range(10**9):
    print(i)
" | head -3

Unlike a plain C program, CPython resets SIGPIPE’s disposition to its default, signal-terminating behavior at interpreter startup specifically so a Python script piped into head dies cleanly the same way any other Unix program does — but code that explicitly catches exceptions around its own output (print, sys.stdout.write) will instead see a BrokenPipeError raised at the Python level rather than the process simply terminating via the signal, since the write call itself fails before the signal has a chance to end the process on the next attempt. A script that wraps its output in a broad try/except block without accounting for this can end up silently swallowing what should have been a normal, expected pipeline shutdown — worth checking specifically if a Python-based tool logs an unexpected error every time it’s piped into something like head rather than exiting quietly the way a shell command would.

Related:

Sources:

Comments