@matheusbozetti/run-concurrent
v1.1.4
Published
Run async functions concurrently with a concurrency limit. A lightweight, type-safe alternative to Promise.all for batch processing and parallel task execution.
Maintainers
Readme
run-concurrent
Lightweight TypeScript utility to run async functions concurrently with a concurrency limit. A smarter alternative to Promise.all for batch processing, rate limiting, and parallel task execution in Node.js.
Zero dependencies | Full TypeScript support | ESM & CJS
Why not just Promise.all?
Promise.all fires every promise at once. With hundreds of tasks (API calls, DB queries, file operations), that means resource exhaustion, rate limit errors, and unpredictable behavior.
runConcurrent gives you:
- Concurrency control — limit how many async operations run at the same time
- Graceful error handling — continue execution even when some tasks fail (
stopOnError: false) - Fail-fast mode — stop immediately on the first error (
stopOnError: true) - Original error propagation — optionally rethrow the original error instead of wrapping it (
throwOriginalError: true) - Ordered results — results come back in declaration order, not execution order
- Strong type inference — preserves tuple types for heterogeneous task arrays
Installation
npm install @matheusbozetti/run-concurrent
# or
pnpm add @matheusbozetti/run-concurrent
# or
yarn add @matheusbozetti/run-concurrentQuick Start
import { runConcurrent } from "@matheusbozetti/run-concurrent";
const results = await runConcurrent(
[
async () => fetch("/api/users").then((r) => r.json()),
async () => fetch("/api/posts").then((r) => r.json()),
async () => fetch("/api/comments").then((r) => r.json()),
],
{ concurrency: 2 }
);
// results: [users, posts, comments] — in order, 2 at a timeAPI
runConcurrent(tasks, options?)
Executes an array of async functions with controlled concurrency.
Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| tasks | Array<() => Promise<T>> | Array of async functions to execute |
| options | RunConcurrentOptions | Configuration (see below) |
Options
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| concurrency | number | 5 | Maximum number of tasks running in parallel |
| stopOnError | boolean | true | If true, stops on first failure and throws. If false, continues and collects errors |
| throwOriginalError | boolean | false | If true, throws/stores the original Error instead of wrapping it in ConcurrencyError |
Return Types
When stopOnError: true (default) — returns Promise<T[]>
const results = await runConcurrent(tasks, { stopOnError: true });
// T[] — array of resolved valuesWhen stopOnError: false — returns Promise<{ data, errorIndexes }>
const results = await runConcurrent(tasks, { stopOnError: false });
// { data: (T | ConcurrencyError)[], errorIndexes: number[] }When stopOnError: false + throwOriginalError: true — errors are the original Error instead of ConcurrencyError
const results = await runConcurrent(tasks, {
stopOnError: false,
throwOriginalError: true,
});
// { data: (T | Error)[], errorIndexes: number[] }Error Handling
Default: ConcurrencyError wrapper
When a task fails, it is wrapped in a ConcurrencyError that includes:
message— the original error messageindex— position of the failed task in the input arrayoriginalError— reference to the originalErrorobject
const results = await runConcurrent(
[
async () => "ok",
async () => { throw new Error("timeout"); },
async () => "ok",
],
{ stopOnError: false }
);
console.log(results.data);
// ["ok", ConcurrencyError { message: "timeout", index: 1 }, "ok"]
console.log(results.errorIndexes);
// [1]Propagating the original error
Use throwOriginalError: true when you need to check instanceof or preserve custom error types:
class ApiError extends Error {
constructor(public statusCode: number) {
super(`HTTP ${statusCode}`);
}
}
// With stopOnError: true — throws the original ApiError
try {
await runConcurrent(
[async () => { throw new ApiError(429); }],
{ stopOnError: true, throwOriginalError: true }
);
} catch (error) {
console.log(error instanceof ApiError); // true
console.log(error.statusCode); // 429
}
// With stopOnError: false — stores the original error in the data array
const results = await runConcurrent(
[async () => { throw new ApiError(429); }],
{ stopOnError: false, throwOriginalError: true }
);
console.log(results.data[0] instanceof ApiError); // trueTypeScript Type Inference
runConcurrent preserves individual return types when tasks are inlined or declared with as const:
// Inlined — TypeScript infers [number, string, boolean]
const results = await runConcurrent([
async () => 42,
async () => "hello",
async () => true,
]);
// With `as const` — same inference
const tasks = [
async () => 42,
async () => "hello",
async () => true,
] as const;
const results = await runConcurrent(tasks);
// [number, string, boolean]Without as const, TypeScript widens the type to (string | number | boolean)[].
Use Cases
- API batch requests — call multiple endpoints without overwhelming the server
- Database bulk operations — insert/update rows with connection pool limits
- File processing — read/write many files without exhausting file descriptors
- Web scraping — crawl pages with controlled request rate
- Microservice orchestration — fan-out requests to downstream services
Testing
pnpm test # or npm test / yarn testTests are written with Vitest.
License
MIT License © Matheus
