Release candidate — 1.0.0-rc.4
← Back to blog

Run Independent API Calls in Parallel: all, any, race

Oleksandr Zhuravlov

A stitch is one endpoint as a typed, validated, resilient function. When one call needs the result of the one before it, you want a sequence — that is linked. But just as often the calls are independent: a dashboard needs a user, their settings, and their feature flags, and none of them waits on the others. Running those in sequence is latency you are paying for nothing.

Promise.all is the reflex, and it gets you the concurrency. What it doesn't get you is everything a stitch already carries: the three calls become three unrelated runs with no shared trace, a failure leaves the other requests running, and you reassemble the error yourself. StitchAPI's parallel family — all, any, race — runs independent stitches concurrently as one group: a shared trace, fail-fast with auto-cancellation, and each member keeping its own retry, timeout, and validation.

The whole family answers two questions any set of calls can have: do I need all of them, or just one? and does "done" mean a success, or any result at all?

CombinatorResolves whenResultFor
allall succeed (fail-fast)named object / tupleindependent fan-out
anythe first success (else all fail)one outputfailover across redundancy
racethe first to settle (win or lose)one outputhedging / fastest-wins

all — independent, all of them

all runs a named bag of stitches concurrently and resolves to a typed object keyed by the names you gave:

const  = ({
    : ,
    : ,
    : ,
});

const { , ,  } = await ({ : { : 1 } });
// user, settings, flags — each typed from its own stitch, no casts

Prefer positional? Pass an array — or just list the stitches as bare arguments — and get a tuple back. The brackets are optional: all([a, b]) and all(a, b) build the identical group, the exact Promise.all shape with the resilience and cancellation added:

// an array — the exact `Promise.all` shape:
const [, ] = await ([, ])({
    : { : 1 },
});

// the same members as bare arguments — identical group, identical tuple:
const [, ] = await (
    ,
    ,
)({
    : { : 1 },
});

Named object, array, or bare arguments — the same combinator; pick whichever reads better. Either way it's Promise.all with the rough edges removed: it's fail-fast — the first member to fail rejects the whole all with that call's StitchError (.status, .url, …) and aborts the others (their in-flight requests are cancelled, not left running) — and each member keeps its own retry, timeout, and validation.

any — the first one that works

any runs its members concurrently and resolves with the first to succeed, ignoring failures until none are left — then it rejects with an AggregateError of every failure. The moment one wins, the losers are aborted. That's failover across interchangeable sources:

// hit both, take whichever returns first; only fail if BOTH do
const  = ([, ]);

const  = await ({ : { : 1 } });

Like all, any and race take their members either way — an array (any([a, b])) or bare arguments (any(a, b)).

This is distinct from a stitch's built-in retry, which re-hits the same endpoint. any is for redundancy across different endpoints — a primary and a mirror, two regions, two providers of the same shape.

race — the first one to finish, win or lose

race resolves or rejects with the first member to settle — success or failure — and cancels the rest. Use it to hedge a latency-sensitive call against a second source and take whichever returns first:

const  = ([, ]);

const  = await ({ : { : 1 } });

The difference from any is what counts as "done": any waits past failures for a success; race takes the first result of any kind. Reach for race when latency is the enemy and a fast failure is still information; reach for any when you want the answer and don't care who provides it.

They nest

Every combinator returns the same callable shape (input?) => Promise<Out> that a stitch does, so a combinator is a valid member of any other — and the types flow straight through. A failover inside a fan-out is just an any as a member of an all:

const  = ({
    : ([, ]), // failover, inside the fan
    : ,
});

const { ,  } = await ({ : { : 1 } });

An all can hold an any; an any can hold a race; and a sequential linked scope can run any of them. The tree goes as deep as the problem, and every node is typed from the stitches at its leaves.

Still composition, not orchestration

A group of concurrent calls is fenced deliberately. The shape is fixed when you write itall/any/race take a list of stitches you already declared; nothing branches, loops, or spawns at runtime. Each member runs as a child run of the group, so the trace draws a fan (the run-identity model) instead of three unrelated calls. Composition adds ordering and concurrency around stitches; it never changes how any one of them behaves alone. That's why this stays runtime stitching, not a workflow platform.

Try it

npm install stitchapi@rc

The parallel family lives at the stitchapi/pipe subpath, so it only enters your bundle when a flow needs it. Each member is a stitch with its own validation and resilience, and the whole group is visible in the event stream.

Related reading: compose dependent calls into one flow, typed errors without try/catch, and runtime stitching vs. workflow platforms.