Skip to content

Async Pipe

Compose async functions into a sequential pipeline where each function receives the output of the previous one.

When to Use

  • Chaining multiple async transformations on a value
  • Building data processing pipelines (fetch → transform → store)
  • Replacing deeply nested .then() chains with a declarative pipeline

The Pattern

Data loading pipeline

javascript
const loadUser = asyncPipe(
  id => fetch(`/api/users/${id}`),
  response => response.json(),
  user => enrichWithProfile(user),
);

const user = await loadUser(42);

Database query pipeline

javascript
const listAccounts = asyncPipe(
  sql => query(sql),
  accounts => accounts.map(withHoldings),
  promises => Promise.all(promises),
  accounts => accounts.map(toRecord),
);

const accounts = await listAccounts(
  AccountQuery.list
);

Process cleanup pipeline

javascript
const killProcessTree = asyncPipe(
  () => pidtree(pid, {
    advanced: true,
    root: true,
  }),
  reverse(),
  map(process => process.pid),
  map(pid => killPromisified(pid)),
  promises => Promise.allSettled(promises),
);

Implementation

Each function in the pipeline receives the resolved value from the previous step. The initial input can be a value or a promise.

javascript
function asyncPipe(...fns) {
  return async function (value) {
    value = await value;
    for (const fn of fns) {
      value = await fn(value);
    }
    return value;
  };
}

Trade-offs

ApproachProsCons
asyncPipeDeclarative, composableHarder stack traces
Chained awaitEasy to debugRepetitive
.then() chainsNo async/await neededDeeply nested

When to avoid: If the pipeline has complex branching or error recovery between steps, sequential await with explicit conditionals is clearer. asyncPipe works best for linear transformations.