Run Async Functions in Sequence
`Promise.all` runs every task at once, but sometimes you need strict ordering: a write that must come after a read, a paginated API that uses the previous response as a cursor, or a queue of migrations that must not interleave. This snippet covers the serial `for...of` loop, the equivalent `reduce`-based one-liner, and a chain helper that pipes each result into the next step. Pick the shape that matches how your data flows.
274 views
8
async function sequentialMap(items, fn) {
const out = [];
for (const item of items) {
out.push(await fn(item));
}
return out;
}
async function fakeFetch(id) {
return new Promise((resolve) => setTimeout(() => resolve(`item-${id}`), 5));
}
sequentialMap([1, 2, 3], fakeFetch).then((rs) => console.log(rs));A plain for...of with await is the clearest way to run async work strictly one after another. Each iteration suspends until the previous promise settles, so the order of out matches the input order regardless of timing. Use this when each step has a side effect that must not interleave (writes, audit logs, rate-limited APIs) or when you simply prefer a readable loop over a functional chain. The trade-off versus Promise.all is wall-clock latency: n items at t ms each take n * t ms instead of t ms.
function serial(items, fn) {
return items.reduce(
(chain, item) => chain.then((acc) => fn(item).then((v) => (acc.push(v), acc))),
Promise.resolve([]),
);
}
async function fakeFetch(id) {
return new Promise((resolve) => setTimeout(() => resolve(`item-${id}`), 5));
}
serial([1, 2, 3], fakeFetch).then((rs) => console.log(rs));Folding reduce over the array, starting from Promise.resolve([]), builds a chain where each .then returns the next promise. The accumulator carries both the running result array and the chain itself, so the function returns a single promise that settles once every step has completed in order. The shape is dense but useful when you want to avoid for await (older targets, lint rules) or to keep the call expression-only. Functionally identical to the for...of version in throughput and ordering.
function pipeAsync(steps) {
return (input) => steps.reduce((p, step) => p.then(step), Promise.resolve(input));
}
const pipeline = pipeAsync([
async (id) => ({ id, raw: `row-${id}` }),
async (row) => Object.assign(row, { upper: row.raw.toUpperCase() }),
async (row) => Object.assign(row, { tag: 'ok' }),
]);
pipeline(7).then((r) => console.log(r));When each step depends on the previous one's output, pipe the steps directly instead of carrying an array. pipeAsync accepts an array of unary async functions and returns a function that threads the input through every step in order. This is the async cousin of synchronous function composition and the right shape for ETL stages, request middleware, or HTTP retries that need to reuse the previous attempt's data. Throw inside any step and the pipeline rejects at that point with the original error stack intact.
