Mastering Promise Concurrency: Techniques to Control Async Tasks in JavaScript

This article explains how to manage Promise concurrency in JavaScript, covering built‑in methods like Promise.all, allSettled, race, any, custom throttling functions, third‑party libraries, generator‑based solutions, and message‑queue approaches to improve performance and user experience.

JavaScript
JavaScript
JavaScript
Mastering Promise Concurrency: Techniques to Control Async Tasks in JavaScript

Promise has become the standard way to handle asynchronous operations in JavaScript. When many async tasks need to run concurrently, controlling Promise concurrency is crucial for performance and user experience.

Browser concurrency limits (typically 6‑8 per domain) can cause requests to be blocked.

Server overload may lead to slow responses or crashes.

Resource contention (e.g., database connections) can cause deadlocks.

Poor user experience due to long loading times.

Therefore, we need to control Promise concurrency while maintaining task efficiency and avoiding excessive system load.

Promise.all : Parallel execution, unified return

Promise.all

accepts an array of Promises, runs them in parallel, and resolves with an array of results when all Promises are fulfilled.

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log(results); // Output: [1, 2, 3]
  });

Applicable scenario: Multiple async tasks without dependencies can run in parallel.

Note: If any Promise rejects, Promise.all rejects immediately with the first rejection reason.

Promise.allSettled : Parallel execution, returns all statuses

Promise.allSettled

behaves like Promise.all but waits for every Promise to settle (fulfilled or rejected) and returns an array describing each outcome.

const promise1 = Promise.resolve(1);
const promise2 = Promise.reject("Error");
const promise3 = Promise.resolve(3);

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    console.log(results);
    /* Output:
    [
      { status: 'fulfilled', value: 1 },
      { status: 'rejected', reason: 'Error' },
      { status: 'fulfilled', value: 3 }
    ]
    */
  });

Applicable scenario: Need results of all Promises regardless of success or failure.

Promise.race : Parallel execution, first settled wins

Promise.race

takes an array of Promises and resolves or rejects as soon as the first Promise settles.

Applicable scenario: Retrieve the fastest result, such as implementing a request timeout.

Promise.any (ES2021): Parallel execution, first fulfilled

Promise.any

resolves with the first fulfilled Promise; if all reject, it rejects with an AggregateError.

Applicable scenario: Need the first successful Promise result.

Custom concurrency control function: limit maximum parallelism

Built‑in methods like Promise.all cannot limit the number of concurrent executions. A custom function can enforce a maximum concurrency limit.

Usage example:

Principle: tasks: array of functions returning a Promise. limit: maximum number of concurrent tasks. results: stores all task results. running: holds currently executing Promises. current: index of the next task to start.

Loop while there are pending tasks or running tasks.

If running.length is below limit and tasks remain, start the next task and push its Promise into running.

Each task.then() adds the result to results and removes the Promise from running.

When running.length reaches limit, await Promise.race(running) waits for any task to finish.

Finally, Promise.all(results) resolves when all tasks are done.

Third‑party libraries: p-limit , async-pool

Several mature libraries simplify Promise concurrency control. p-limit: a lightweight concurrency‑control library.

async-pool

: supports multiple concurrency strategies.

Using Generator functions and yield

Generator functions can pause and resume execution; combined with yield, they enable fine‑grained concurrency control.

async function* taskGenerator(tasks) {
  for (const task of tasks) {
    yield task();
  }
}

async function runTasks(tasks, limit) {
  let pool = [];
  let results = [];
  for await (let result of taskGenerator(tasks)) {
    pool.push(result);
    results.push(result);
    if (pool.length >= limit) {
      await Promise.race(pool);
      pool = pool.filter(p => p.status != 'fulfilled' && p.status != 'rejected'); // manual cleanup
    }
  }
  return Promise.all(results);
}

Using message queues

For a very large number of async tasks where some delay is acceptable, you can employ message queues (e.g., RabbitMQ, Kafka) to enqueue tasks and let multiple consumers process them in parallel.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptconcurrencyAsyncPromise
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.