Promise WaterfallLoading saved progress…

Promise Waterfall

A promise waterfall runs an array of async functions in sequence, passing each function's resolved result as the input to the next, and resolves with the value the last function returns. The name is the picture: a value falls from one task straight down into the next — never sideways — so only one task runs at a time. It is a well-known composition pattern; the popular async library ships it as async.waterfall.

Signature

type Task = (value: any) => any | Promise<any>;

// Runs tasks left-to-right: task[0] receives initialValue, and each later task
// receives the previous task's resolved value. Resolves with the final value.
function promiseWaterfall(tasks: Task[], initialValue: unknown): Promise<unknown>;

Examples

// Each task transforms the running value; the chain resolves with the last one.
promiseWaterfall([(n) => n + 1, (n) => n * 2, (n) => n - 3], 5)
  .then(console.log); // 9   — ((5 + 1) * 2) - 3
// Tasks can be async. Each task waits for the previous promise to resolve.
const loadUser = (id) => Promise.resolve({ id, name: 'Ada' });
const nameOf = (user) => user.name.toUpperCase();
promiseWaterfall([loadUser, nameOf], 42).then(console.log); // 'ADA'
// An empty task list resolves with the initial value, untouched.
promiseWaterfall([], 'seed').then(console.log); // 'seed'
// A rejection stops the chain — later tasks never run.
promiseWaterfall([(n) => n + 1, () => Promise.reject('boom'), (n) => n * 2], 0)
  .catch(console.log); // 'boom'  (the * 2 task never runs)

Notes

  • Strictly one at a time. Tasks never overlap. Each task starts only after the previous task's returned promise resolves — the opposite of Promise.all, which starts everything at once.
  • The value is threaded. task[i] receives whatever task[i - 1] resolved to. Only task[0] sees initialValue.
  • First error wins. A synchronous throw or a rejected promise from any task rejects the waterfall immediately with that reason; the remaining tasks are skipped.
  • Always returns a promise — even for an empty list. Callers can always .then or await the result.
  • Tasks may be sync or async. A task that returns a plain value behaves the same as one that returns a promise; awaiting handles both.

FAQ

How is a promise waterfall different from Promise.all?
Promise.all runs every task in parallel and resolves with an array of all results. A waterfall runs tasks one at a time, feeds each task the previous task's resolved result, and resolves with just the final value.
What happens if one of the tasks throws or rejects?
The chain stops at that task and the returned promise rejects with that reason. Any tasks after the failing one are never run.
What does an empty task list resolve to?
It resolves with the initialValue you passed in, unchanged. The function always returns a promise, so you can await it either way.
Loading editor…