mrkeyoor.com_
Sat 08 Aug 21:02 UTC
npmUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5The runtime API is one default function plus one stop symbol, and version 3's source is a short for-of loop whose behavior is easy to audit. The major migration to ESM is the meaningful compatibility break. There is also tension between the declaration's array result and the implementation's original-iterable return for non-array inputs, which lowers confidence at the edge.
Docs4/5The README concisely states serial input behavior, promise resolution, rejection rules, ignored callback values, original-input return, callback index, and the stop sentinel. It also recommends p-map when side effects are unnecessary and links adjacent utilities. It does not call out ESM migration, AsyncIterable exclusion, cancellation absence, or the non-array typing mismatch.
Maintenance2/5Version 3.0.0 was published on April 9, 2021 and the repository's last push was July 8, 2022. GitHub currently shows no open issues or pull requests and the package has no dependencies, so little maintenance is required, but there is no recent release activity to resolve modern runtime, typing, or cancellation questions.
Ecosystem4/5The package recorded 5,187,487 npm downloads in the latest complete week and belongs to the well-known promise-fun family, with direct links to p-map-series, p-series, p-reduce, p-pipe, and p-map. Much of that volume may be transitive, and the API is too small to create a plugin ecosystem, but replacement and migration paths are obvious.

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
Skip it if

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

PackageRegistryPick it when
p-map-seriesnpmYou need serial execution but want an array of transformed return values
p-seriesnpmYou have a sequence of different promise-returning functions rather than one iterator over values
p-mapnpmYou want transformed results with configurable concurrency instead of one-at-a-time execution