mrkeyoor.com_
Wed 23 Sept 00:37 UTC
npmUtilsupdated 22 Sept 2026

p-each-series review

p-each-series 3.0.0 runs one iterator call at a time over a synchronous iterable. It awaits each input value, calls the iterator with that resolved value and a zero-based index, then waits for the iterator before advancing. The iterator's return value is discarded except for the exported `pEachSeries.stop` symbol, which ends the loop. The final result is the original iterable, not collected callback results. Version 3 is the April 2021 release that moved the package to pure ESM and raised the stated floor to Node 12. No feature release has followed, and the repository's last push was in 2022.

Verdict

p-each-series 3.0.0 installed in 0.5 seconds, used 1 MB, bundled to 0.2 KB gzipped, and produced 0 audit findings in our sandbox. Install it only when the stop sentinel and side-effect-only contract earn their keep; a plain `for...of` loop expresses most serial workflows with fewer surprises.

We installed it

Lab card: what happened when we installed p-each-seriesScreenshot of p-each-series documentation
Install✓ · 0.5s1 package on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser0.2 KBgzipped (0.3 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does p-each-series install cleanly?

Yes. In a fresh container with an empty cache, npm install p-each-series finished in 0.5s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does p-each-series add to a browser bundle?

0.2 KB gzipped (0.3 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does p-each-series work with both ESM and CommonJS?

Yes. Both import 'p-each-series' and require('p-each-series') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does p-each-series include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

p-each-series or p-map: which should you use?

p-map: Use it when results matter or several items should run concurrently under an explicit limit. p-each-series 3.0.0 installed in 0.5 seconds, used 1 MB, bundled to 0.2 KB gzipped, and produced 0 audit findings in our sandbox.

When should you not use p-each-series?

You build an array of already-started promises and expect their underlying work to run serially. Promise executors start when created, before p-each-series awaits them.

API stability4/5The runtime surface is one default function plus the `stop` symbol, and version 3 has remained unchanged since April 2021. The loop's behavior is easy to inspect: await the input, await the iterator, check the sentinel, return the original iterable. The major release was breaking because it became pure ESM and required Node 12. Its TypeScript return type promises an array even though runtime accepts and returns any synchronous iterable, which weakens the otherwise simple contract.
Docs3/5The README gives installation, one ordered side-effect example, the complete function signature, rejection behavior, iterator indexing, the stop sentinel, and links to adjacent promise utilities. It does not warn that eagerly created promises may already be running, does not discuss generators or the declared return-type mismatch, and leaves retries, cancellation, and cleanup entirely to the reader. With only a few lines of source, the missing edge cases are still easy to verify.
Maintenance2/5Version 3.0.0 was published on April 9, 2021, npm metadata was last modified in 2022, and GitHub shows the last repository push on July 8, 2022. The repository remains unarchived with 0 open issues and pull requests. No later release has refreshed the Node support range or fixed the iterable return type. Stability may explain some silence; there is still no current maintenance signal for a new adopter.
Ecosystem3/5The npm endpoint counted 5,335,479 downloads in the latest completed week, while GitHub reports 52 stars. It belongs to the promise-fun family and links directly to p-map-series, p-series, p-pipe, p-waterfall, p-reduce, and p-map, so migration to a neighboring control-flow shape is straightforward. The package itself has no hooks or integrations, and native async functions plus `for...of` cover its core behavior in every supported runtime.

Use it if

  • Side effects such as ordered writes or rate-limited calls must finish strictly one iterator invocation at a time.
  • Input may mix ordinary values and promises, and callback results are irrelevant to the caller.
  • A callback needs both the resolved value and its zero-based position in the iterable.
  • Iteration should stop cleanly on a sentinel while the function still returns the original input object.
Skip it if

Setup reality

We installed p-each-series 3.0.0 in a fresh Node 22 Bookworm sandbox. npm completed in 0.5 seconds and left 1 package using 1 MB on disk. The package is 24 KB unpacked with 0 direct dependencies and 0 peer dependencies. npm audit found 0 known vulnerabilities. It declares Node 12 or newer and bundles TypeScript declarations.

Version 3 is an ESM package with an exports map. ESM import worked, and require() also worked in our Node 22 check, but older CommonJS runtimes cannot assume modern Node's ESM require behavior. There is no native build, config file, credential, or runtime service. The browser bundle measured 0.3 KB minified and 0.2 KB gzipped, so installation weight is not the decision point.

Serial execution starts inside the iterator. If fetchA() and fetchB() are called while constructing [fetchA(), fetchB()], both operations begin before p-each-series sees the array. Pass plain values and start the async operation in the iterator, or use a lazy synchronous generator that creates each promise on demand. The function rejects immediately when an input promise or iterator call rejects; it has no retry, timeout, cancellation, or rollback policy.

Returning pEachSeries.stop ends the loop after the current item. The fulfilled value remains the same original iterable, including items never visited. Callback values are otherwise ignored. A Set or generator is accepted at runtime, although the bundled declaration says the promise resolves to an array, which is narrower than the JavaScript implementation. For new code, a for...of loop is often clearer and avoids that type mismatch.

Patterns

Wait for one side effect before starting the next run-side-effects-serially

import pEachSeries from 'p-each-series';

await pEachSeries(records, async (record) => {
  await saveRecord(record);
});

Pass unstarted values in `records`. The iterator call is where each asynchronous side effect should begin.

Read the zero-based iterator index use-index

await pEachSeries(files, async (file, index) => {
  await upload(file, { position: index });
});

The index increments once per visited element and stops increasing after a rejection or stop sentinel.

Stop after a matching value stop-early

await pEachSeries(rows, async (row) => {
  await inspect(row);
  if (row.status === 'terminal') return pEachSeries.stop;
});

The current iterator finishes before the sentinel is checked. Later elements are not visited.

Keep the original iterable as the result understand-return-value

const input = ['a', 'b', 'c'];
const result = await pEachSeries(input, async value => write(value));

console.log(result === input); // true

Iterator results are discarded. The function fulfills with the exact input object, including unvisited items after an early stop.

Start promises inside the iterator avoid-eager-promises

const urls = ['/a', '/b', '/c'];
await pEachSeries(urls, url => fetch(url));

Writing `[fetch('/a'), fetch('/b')]` starts both requests immediately, so wrapping that array does not serialize network activity.

Use a synchronous generator for lazy inputs produce-promises-lazily

function* requests(urls) {
  for (const url of urls) yield fetch(url);
}

await pEachSeries(requests(urls), response => consume(response));

A synchronous generator creates each fetch when the loop asks for its next value. Async generators are not accepted by version 3.0.0.

Attach context to the first failure handle-failure

try {
  await pEachSeries(records, async (record, index) => {
    try {
      await saveRecord(record);
    } catch (cause) {
      throw new Error(`record ${index} failed`, { cause });
    }
  });
} catch (error) {
  report(error);
}

The first rejected input or iterator call stops the loop. The package does not roll back side effects from earlier items.

Retry one item before advancing retry-each-item

await pEachSeries(records, async record => {
  for (let attempt = 1; ; attempt += 1) {
    try {
      await saveRecord(record);
      return;
    } catch (error) {
      if (attempt === 3) throw error;
    }
  }
});

Retries stay inside the iterator, so item 2 cannot begin while item 1 is retrying. Add delay and retry classification for a real remote service.

Bound the time spent on one item add-timeout

await pEachSeries(urls, async url => {
  const response = await fetch(url, { signal: AbortSignal.timeout(5_000) });
  await consume(response);
});

p-each-series supplies no timeout or abort signal. Cancellation must come from the operation being awaited.

Collect results explicitly when order matters collect-results

const output = [];
await pEachSeries(inputs, async input => {
  output.push(await transform(input));
});

The callback's return value is ignored. For routine mapping, `p-map` with concurrency 1 or a plain loop states this intent more directly.

Process a Set in insertion order iterate-set

const ids = new Set(['a', 'b', 'c']);
const result = await pEachSeries(ids, id => remove(id));

console.log(result === ids); // true

Runtime returns the original Set. The bundled declaration's array return type does not describe this case accurately.

Use the native equivalent for ordinary work replace-with-native-loop

for (const [index, value] of values.entries()) {
  await run(value, index);
}

A native loop already serializes iterator calls and makes `break`, collected results, and error handling visible without a dependency.

Alternatives

PackageRegistryPick it when
p-mapnpmUse it when results matter or several items should run concurrently under an explicit limit.
p-seriesnpmUse it when the input is a sequence of promise-returning functions rather than values passed to one iterator.
asyncnpmUse it when the workflow also needs queues, retries, waterfalls, or callback-compatible control flow.
bluebirdnpmUse its collection helpers only in an existing Bluebird codebase; adding a full promise library for one serial loop is hard to justify.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.