Files
skills/docs/foundation/flow/streams/src/pipeline/safe-branch/safe-branch.md
2026-05-29 08:20:56 +01:00

4.8 KiB

safeBranch

(no submodules)

Purpose

safeBranch is a pipeline operator that wraps a sub-pipeline factory in a fault boundary. It runs the supplied factory(input) to construct an inner ReadableStream, then forwards every chunk it produces to the wrapper's output stream. If the inner pipeline throws — synchronously while the factory is being invoked, or asynchronously while its stream is being read — the wrapper's output stream closes cleanly (no further chunks emitted) and the error is forwarded to options.onError as a StreamBranchFailedError whose cause is the original error.

The outer pipeline never observes the inner error. This is the difference between a normal pipeThrough (which propagates errors and tears down the pipeline) and safeBranch (which contains them). Use it for opportunistic side branches — analytics, mirroring, telemetry — where a failure must not bring down the primary data path.

Features

  • Isolates errors from inner sub-pipelines so the outer pipeline keeps running
  • Forwards wrapped errors to options.onError as StreamBranchFailedError
  • Preserves the original error via the cause chain
  • Output stream closes cleanly after a branch error — no half-emitted state
  • Silent mode when onError is omitted
  • Synchronous factory throws are caught and reported the same way

Usage: Basic Example

import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
import { safeBranch } from "@suaveplan/streams/pipeline/safe-branch/safe-branch";
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";

const wrapped = safeBranch<number, number>(
    (input) =>
        input.pipeThrough(
            new TransformStream<number, number>({
                transform(chunk, c) {
                    c.enqueue(chunk * 2);
                },
            })
        )
);

const out = wrapped(fromArray([1, 2, 3]));
const result = await toArray(out); // [2, 4, 6]

Usage: Advanced Example

import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
import { safeBranch } from "@suaveplan/streams/pipeline/safe-branch/safe-branch";
import { StreamBranchFailedError } from "@suaveplan/streams/errors/stream-error";
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";

let lastError: StreamBranchFailedError | undefined;

const analyticsBranch = safeBranch<number, number>(
    (input) =>
        input.pipeThrough(
            new TransformStream<number, number>({
                transform(chunk, c) {
                    if (chunk < 0) throw new Error("negative not allowed");
                    c.enqueue(chunk);
                },
            })
        ),
    {
        onError: (err) => {
            lastError = err as StreamBranchFailedError;
        },
    }
);

const collected = await toArray(analyticsBranch(fromArray([1, -2, 3])));
// collected captures whatever was emitted before the error;
// the outer pipeline does not throw.
console.log(lastError?.code); // "STREAM.BRANCH_FAILED"
console.log(lastError?.cause); // Error: negative not allowed

API Reference

safeBranch<TIn, TOut>(factory, options?)

  • factory: (input: ReadableStream<TIn>) => ReadableStream<TOut> — constructs the inner sub-pipeline. May throw synchronously; failures are isolated.
  • options.onError?: (err: unknown) => void — invoked with a StreamBranchFailedError when the inner pipeline errors. The original error is exposed via err.cause.
  • Returns (input: ReadableStream<TIn>) => ReadableStream<TOut> — a wrapper function suitable for use anywhere a stream-shape transform is accepted.
  • Throws Never. All errors are converted to StreamBranchFailedError and either delivered to onError or silently swallowed.

Implementation Notes

  • The wrapper acquires a single reader against the inner stream and pulls chunk-by-chunk inside the outer stream's pull callback. This preserves backpressure end-to-end: the outer consumer's pull cadence drives the inner sub-pipeline's read cadence.
  • On error, the wrapper sets a closed flag, closes the outer controller, and calls onError. Subsequent pulls return done without re-invoking the inner reader.
  • Cancellation of the wrapper propagates to the inner reader via reader.cancel(reason). Cancellation errors from the inner stream are swallowed — they are part of the isolated branch by definition.
  • The error code is STREAM.BRANCH_FAILED (registered in errors/stream-error.ts). Inspect err.code to detect a branch failure programmatically; inspect err.cause to recover the original error.
  • safeBranch does not retry or restart the inner pipeline. After failure, the wrapper is terminal. To retry, pair safeBranch with @suaveplan/retry upstream of the factory.