Efficient Concurrency Control in JavaScript: 3 Practical Solutions
Managing asynchronous operations efficiently is a critical part of modern JavaScript development. In scenarios where multiple API requests need to be coordinated, choosing the right concurrency strategy can impact performance and code clarity. Below, we explore three effective methods for handling concurrent requests in JavaScript, each suited to different dependencies and complexity levels.
Solution 1: Use Promise.all()
to Run Tasks in Parallel
The simplest and most direct method is to use Promise.all()
to initiate Request A and Request B simultaneously, and then proceed to Request C once both have completed.
async function fetchData() {
try {
// Execute A and B in parallel
const [resultA, resultB] = await Promise.all([
fetchRequestA(),
fetchRequestB()
]);
// Proceed with C after A and B complete
const resultC = await fetchRequestC(resultA, resultB);
return resultC;
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
Pros:
- Clean and readable code
- Perfect when Request C depends on A and B
Cons:
- Inefficient if Request C does not depend on A or B — it waits unnecessarily
Solution 2: Trigger Requests Simultaneously and Handle Dependencies Later
If Request C is independent of A and B, you can start all three requests simultaneously, then wait only for A and B to finish before processing C’s result.
async function fetchData() {
try {
// Start all requests at once
const promiseA = fetchRequestA();
const promiseB = fetchRequestB();
const promiseC = fetchRequestC();
// Wait for A and B to complete
const [resultA, resultB] = await Promise.all([promiseA, promiseB]);
// Retrieve C’s result (may already be done)
const resultC = await promiseC;
return { resultA, resultB, resultC };
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
Pros:
- Non-blocking: C starts immediately without waiting for A and B
- Great when C is unrelated to A and B
Cons:
- Slightly more complex code
- You must ensure C truly doesn’t rely on A or B
Solution 3: Build a Custom Concurrency Controller
For advanced scenarios—such as limiting the number of concurrent requests—you can implement a custom request controller to manage concurrency more granularly.
class RequestController {
constructor(maxConcurrency = 2) {
this.runningCount = 0;
this.maxConcurrency = maxConcurrency;
this.queue = [];
}
async addRequest(requestFn) {
if (this.runningCount >= this.maxConcurrency) {
await new Promise(resolve => this.queue.push(resolve));
}
this.runningCount++;
try {
return await requestFn();
} finally {
this.runningCount--;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
}
}
}
// Usage
async function fetchData() {
const controller = new RequestController();
const promiseA = controller.addRequest(fetchRequestA);
const promiseB = controller.addRequest(fetchRequestB);
await Promise.all([promiseA, promiseB]);
const resultC = await fetchRequestC();
return resultC;
}
Pros:
- Precise control over concurrency
- Scalable and reusable as a utility class
Cons:
- Requires more code and setup
- Best suited for applications needing custom concurrency management
Choosing the Right Solution
Scenario | Recommended Approach |
---|---|
Request C depends on A and B results | ![]() |
Request C is independent of A and B | ![]() |
Need to limit concurrent requests or reuse | ![]() |
By selecting the appropriate strategy based on your application’s data flow and performance requirements, you can enhance both efficiency and maintainability in your asynchronous JavaScript code.