A tiny, typed concurrency limiter. Run N async tasks at a time over any iterable, get results back in input order, and stop early with an AbortSignal.
- Zero dependencies, ESM + CJS, full TypeScript types.
- Ordered results — the output array lines up with the input, no matter what order tasks finish in.
- Lazy — works with async iterables and infinite generators; items are only pulled as capacity frees up.
- Two failure modes —
poolfails fast likePromise.all,pool.settledreports every outcome likePromise.allSettled. - ~1 kB minified, no
setTimeout, no queue library, no globals.
npm install @dustfillerr-code/async-poolimport { pool } from '@dustfillerr-code/async-pool';
const urls = ['/a', '/b', '/c', '/d', '/e'];
// At most 2 requests in flight; results come back in `urls` order.
const bodies = await pool(urls, (url) => fetch(url).then((r) => r.text()), {
concurrency: 2,
});The worker receives the index too, and may be synchronous:
const labelled = await pool(urls, (url, index) => `${index}: ${url}`);pool rejects on the first failing task. When you want every outcome instead, use pool.settled (also exported as settled):
import { pool } from '@dustfillerr-code/async-pool';
const results = await pool.settled(urls, (url) => fetch(url), { concurrency: 4 });
const ok = results.filter((r) => r.status === 'fulfilled');
const failed = results.filter((r) => r.status === 'rejected');Each entry is { status: 'fulfilled', value } or { status: 'rejected', reason }, in input order.
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
await pool(urls, (url) => fetch(url, { signal: controller.signal }), {
signal: controller.signal,
});
} catch (error) {
// rejects with `controller.signal.reason`
}Aborting stops the pool from scheduling new tasks. It cannot cancel work that is already running — pass the signal into your worker as well, as above, if the work itself needs to be interruptible.
Because items are pulled lazily, the source can be an async iterable or an effectively unbounded generator. Only concurrency items are ever pulled ahead:
async function* rows(cursor: string) {
while (cursor) {
const page = await fetchPage(cursor);
yield* page.rows;
cursor = page.next;
}
}
const processed = await pool(rows(start), (row) => index(row), { concurrency: 8 });| Parameter | Type | Description |
|---|---|---|
items |
Iterable<T> | AsyncIterable<T> |
Source of work. Pulled lazily. |
worker |
(item: T, index: number) => R | PromiseLike<R> |
Turns one item into a result. |
options.concurrency |
number |
Max tasks in flight. Positive integer or Infinity. Default 4. |
options.signal |
AbortSignal |
Stops scheduling new tasks. |
Resolves with one result per item, in input order.
Rejects as soon as any task rejects. Like Promise.all, tasks already in flight keep running but their results are discarded, and no further items are pulled. Rejections from those in-flight tasks are absorbed, so they never surface as unhandled rejections.
Same arguments. Never rejects because of a failing task — every task that started gets an entry, in input order:
type PoolSettledResult<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };If aborted, the array covers only the items that actually started. It still rejects if the source iterable itself throws, or if the arguments are invalid.
Also available as a named export: import { settled } from '@dustfillerr-code/async-pool'.
| Condition | Result |
|---|---|
concurrency is not a positive integer or Infinity |
rejects with TypeError |
items is not iterable |
rejects with TypeError |
worker is not a function |
rejects with TypeError |
| Signal already aborted | pool rejects with signal.reason; pool.settled resolves with [] |
| Source iterable throws | rejects with that error, in both modes |
- Ordering is by input index, not completion. A slow first item does not hold up later ones from running, only from being returned.
- The source iterator is closed (
return()) when the pool stops early, sotry/finallycleanup in your generators runs. concurrency: Infinitypulls the whole source eagerly — the same asPromise.all(Array.from(items).map(worker)).- No timers. The pool is driven purely by task settlement, so it adds nothing to the event loop.
npm install
npm run lint
npm run typecheck
npm test
npm run buildMIT © dustfillerr-code