p-each-series
p-each-series is a tiny ESM function that walks a synchronous iterable in order, awaits each input value, calls an iterator with the resolved value and zero-based index, awaits the iterator's return, and only then advances. It is meant for ordered side effects such as writes or rate-limited requests. It returns the original iterable and exposes a sentinel that lets the iterator stop early.
The helper is correct and pleasantly tiny for ordered side effects in an ESM codebase. A plain loop is often clearer, and any need for results, concurrency, cancellation, or recovery points to another tool.
Use it if
- Side effects must run strictly one at a time and in input order
- Inputs may themselves be promises that should resolve immediately before their iterator call
- You want a small explicit helper with an index and an early-stop sentinel
- Your project is already ESM and supports Node 12 or newer
- You want transformed results; the README says iterator return values are ignored and the fulfilled value is the original input, so use p-map-series instead
- Tasks can safely overlap; the README explicitly recommends p-map when side effects are not the reason for serial execution
- You need bounded concurrency, cancellation, retries, timeouts, or progress events; the implementation is one for-of loop and provides none of those controls
- Your source is an AsyncIterable; the implementation uses ordinary `for...of`, not `for await...of`
- Your project still loads dependencies with CommonJS `require`; version 3 declares `type: module` and exports only its ESM index file
Setup reality
`npm install p-each-series` installs version 3.0.0 with no runtime dependencies and a Node 12 or newer engine declaration. Version 3 is ESM-only, so use `import pEachSeries from 'p-each-series'`; `require('p-each-series')` is not the supported entry. The behavior is intentionally narrower than its name may suggest. The input must be a synchronous Iterable such as an Array, Set, or generator. Each yielded value may be a promise and is awaited before the callback receives it. The callback then runs with `(value, index)`, and its result is awaited only to sequence work. Ordinary results are discarded. When everything finishes, the source object itself is returned rather than a new array of callback results. The published declaration describes an array result even though the JavaScript implementation returns the original iterable, so relying on the result with Set or generator input is risky; in practice, treat the return as a completion signal and keep your input separately. Any rejected input promise or thrown or rejected iterator call immediately rejects the whole operation. There is no cleanup hook, retry, timeout, AbortSignal, or record of the last completed item. Returning `pEachSeries.stop` ends iteration successfully but still returns the original input, so track early termination yourself if the caller must know. Because work is serial, total duration is roughly the sum of task durations. For many cases, a plain `for...of` loop with `await` is just as clear and avoids a dependency.
Patterns
Run ordered side effects one at a timerun-serial-side-effects
import pEachSeries from 'p-each-series';
await pEachSeries(records, async record => {
await saveRecord(record);
});Each callback settles before the next begins. Callback return values are not collected.
Use the zero-based iterator indexuse-item-index
await pEachSeries(files, async (file, index) => {
await upload(file, { sequence: index + 1 });
});The index counts yielded items, including the item that returns the early-stop sentinel.
Resolve promised input values in sequenceawait-promised-inputs
const inputs = [loadFirst(), loadSecond(), loadThird()];
await pEachSeries(inputs, async value => {
await consume(value);
});Creating promises up front may start all underlying operations immediately. Use a lazy generator of values or functions when startup itself must be serial.
Stop successfully at a matching itemstop-early
let found;
await pEachSeries(items, async item => {
if (await matches(item)) {
found = item;
return pEachSeries.stop;
}
});Stopping resolves successfully and returns the original input. Keep separate state when callers need the matched item or completion status.
Handle the first failed side effectfail-fast
try {
await pEachSeries(jobs, runJob);
} catch (error) {
reportFailure(error);
throw error;
}A thrown error, rejected callback, or rejected input stops iteration immediately. The package provides no automatic rollback or resume cursor.
Collect failures without stopping the seriescontinue-after-errors
const failures = [];
await pEachSeries(jobs, async job => {
try {
await runJob(job);
} catch (error) {
failures.push({ job, error });
}
});
if (failures.length) throw new AggregateError(failures.map(x => x.error));Errors must be handled inside the iterator to continue. Decide whether later side effects remain valid after an earlier failure.
Walk a synchronous generator lazilyconsume-generator-lazily
function* pendingJobs() {
for (const row of rows) {
if (row.pending) yield row;
}
}
await pEachSeries(pendingJobs(), processJob);Synchronous generators are supported. Async generators are not because the implementation uses `for...of`.
Add a fixed delay between requestsspace-api-calls
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
await pEachSeries(ids, async (id, index) => {
if (index > 0) await wait(250);
await api.update(id);
});This is simple spacing, not adaptive rate limiting. It does not inspect response headers, retry 429s, or support cancellation.
Use completion without expecting mapped outputpreserve-input
const input = ['a', 'b', 'c'];
const returned = await pEachSeries(input, writeValue);
console.assert(returned === input);The implementation returns the original object. Use p-map-series when iterator results are the desired output.
Invoke queued functions seriallyrun-functions-in-order
const tasks = [
() => migrateSchema(),
() => backfillRows(),
() => rebuildIndex(),
];
await pEachSeries(tasks, task => task());For a fixed list of different functions, the related p-series package expresses this intention more directly.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| p-map-series | npm | You need serial execution but want an array of transformed return values |
| p-series | npm | You have a sequence of different promise-returning functions rather than one iterator over values |
| p-map | npm | You want transformed results with configurable concurrency instead of one-at-a-time execution |