mrkeyoor.com_
Thu 06 Aug 00:59 UTC
npmUtilsupdated 05 Aug 2026

p-map

p-map runs an async function over a list of inputs with a concurrency cap. Promise.all fires everything at once and Promise.allSettled hides which input failed; pMap(items, mapper, {concurrency: 5}) keeps at most five mappers in flight, returns results in input order, and gives you real choices about errors: fail fast, collect everything into an AggregateError, or skip bad items with pMapSkip. pMapIterable does the same as a streaming async iterable with backpressure, so results are consumed as they arrive instead of after the whole batch. Zero dependencies, a couple of KB.

Verdict

The cleanest way to do bounded-concurrency async mapping in Node, and at about 1 KB with zero dependencies there is little reason to hand-roll it. Just remember it is a batch primitive, not a rate limiter or a job queue, and that CJS projects cannot use current versions.

API stability4/5The (input, mapper, options) signature has been unchanged for years; majors were about ESM-only packaging and Node floors, plus additive features like pMapIterable and signal rather than reshapes.
Docs4/5The README documents every option with types, defaults, and honest caveats (the stopOnError concurrency caveat is spelled out), plus a rate-limiting recipe; there is no separate site and no need for one.
Maintenance4/5Pushed July 2026 with 8 open issues (12 counting PRs); sindresorhus maintains it as part of the promise-fun collection, so activity is steady but it is a single volunteer.
Ecosystem4/5Around 80M weekly downloads and deep placement in tooling dependency trees, with the sibling p-* packages (p-limit, p-queue, p-throttle, p-retry) composing with it for the adjacent problems.

Use it if

  • You are hammering an API or database with hundreds of async calls and need 'at most N at a time' without writing a queue: this is the exact problem p-map solves
  • You want failures aggregated instead of losing them: stopOnError: false waits for everything and throws one AggregateError listing every rejection
  • You need cancellation mid-batch: the signal option wires an AbortController through the whole map
  • You consume results slower than you produce them: pMapIterable's backpressure option stops the mappers from racing ahead of the consumer
Skip it if

Setup reality

npm install p-map, one import, zero dependencies, bundled TypeScript types; there is genuinely nothing else to set up. The traps are semantic rather than mechanical. It is ESM only, so CommonJS callers get ERR_REQUIRE_ESM and old tutorials pointing at v4 still circulate. With stopOnError: true (the default) already-started mappers keep running after the rejection propagates, and with infinite default concurrency over a sync iterable every mapper starts immediately, so an unbounded pMap behaves like Promise.all plus surprises; in practice you should always pass concurrency. AggregateError handling also means checking error.errors, which teams forget in catch blocks.

Patterns

Map with a concurrency caplimit-concurrency

import pMap from 'p-map';

const results = await pMap(
  urls,
  async url => {
    const response = await fetch(url);
    return response.json();
  },
  {concurrency: 5}
);

ESM only: require('p-map') throws in CommonJS. Results come back in input order regardless of completion order. Default concurrency is Infinity, so always set it; otherwise you have rebuilt Promise.all.

Run everything and aggregate failurescollect-all-errors

import pMap from 'p-map';

try {
  await pMap(jobs, runJob, {concurrency: 4, stopOnError: false});
} catch (error) {
  // AggregateError: every rejection, not just the first
  for (const single of error.errors) {
    console.error(single.message);
  }
}

With the default stopOnError: true the first rejection propagates but already-started mappers keep running in the background; stopOnError: false waits for all and throws one AggregateError.

Cancel a batch with AbortControllerabort-with-signal

import pMap from 'p-map';

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);

try {
  await pMap(items, process, {concurrency: 8, signal: controller.signal});
} finally {
  clearTimeout(timeout);
}

Aborting rejects the pMap promise with the abort reason; it does not magically stop in-flight mappers unless they also observe the signal, so pass it into fetch and friends too.

Drop failures from the result instead of throwingskip-failed-items

import pMap, {pMapSkip} from 'p-map';

const pages = await pMap(
  urls,
  async url => {
    try {
      return await fetchPage(url);
    } catch {
      return pMapSkip;
    }
  },
  {concurrency: 5}
);
// pages contains only the successes, order preserved

pMapSkip is a sentinel return value, not an option; the skipped slots are removed from the output array entirely rather than left as undefined holes.

Consume results as they are readystream-results

import {pMapIterable} from 'p-map';

for await (const post of pMapIterable(postIds, getPostMetadata, {concurrency: 8})) {
  render(post);
}

Still yields in input order, so one slow early item delays later finished ones; the win over pMap is starting to consume before the whole batch completes, with bounded memory.

Stop mappers racing ahead of a slow consumerbackpressure-control

import {pMapIterable} from 'p-map';

for await (const row of pMapIterable(ids, fetchRow, {
  concurrency: 10,
  backpressure: 20
})) {
  await database.insert(row); // slow consumer
}

backpressure caps how many finished-but-unconsumed results can pile up (default equals concurrency, cannot be lower); without it a fast mapper and slow consumer buffer the whole dataset.

Map over an async iterable sourceasync-iterable-input

import pMap from 'p-map';

async function * readQueue() {
  while (await queue.hasMessages()) {
    yield queue.next();
  }
}

const outcomes = await pMap(readQueue(), handleMessage, {concurrency: 3});

Input can be any sync or async iterable, and each yielded item is awaited before the mapper runs, so an iterable of promises also works.

Use the index argument in the mapperuse-item-index

import pMap from 'p-map';

const uploaded = await pMap(
  files,
  async (file, index) => {
    console.log(`starting ${index + 1}/${files.length}`);
    return upload(file);
  },
  {concurrency: 2}
);

The mapper receives (element, index) only; there is no third 'array' argument like Array#map, so close over the input if you need it.

Combine with p-throttle for per-second limitsrate-limit-requests

import pThrottle from 'p-throttle';
import pMap from 'p-map';

const throttle = pThrottle({limit: 10, interval: 1000, strict: true});

const results = await pMap(inputs, throttle(callApi), {concurrency: 5});

concurrency bounds promises in flight, not calls per second; an API with a requests-per-second quota needs the throttle wrapper too, exactly as the README's recipe shows.

Bounded-concurrency for side effects onlychunked-side-effects

import pMap from 'p-map';

await pMap(
  staleKeys,
  key => cache.delete(key),
  {concurrency: 20, stopOnError: false}
);

Nothing requires you to use the returned array; for pure side-effect fan-out, stopOnError: false plus a caught AggregateError gives you a full failure report in one pass.

Alternatives

PackageRegistryPick it when
p-limitnpmYou want the lower-level primitive: one limit() wrapper you apply to arbitrary promise-returning calls, not a map over a list.
p-queuenpmYou need an ongoing queue with priorities, pause/resume, rate intervals, and timeouts rather than a single batch.
p-allnpmYou have an array of different functions to run with limited concurrency instead of one mapper over many inputs.