-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
195: Queueing and Rate-Limiting Async Operations
Imagine you're building a tool that needs to sync a thousand user profiles from a third-party API. In a perfect world, you'd just map over your array of IDs and call await Promise.all(requests). But in the real world, APIs have rate limits. If you hit a server with 1,000 concurrent requests, you're not going to get your data—you're going to get a 429 "Too Many Requests" error, or worse, a temporary IP ban.
We need a way to say: "I have 1,000 things to do, but I only want 3 of them happening at any single moment."
The "Fire and Forget" disaster
Let's start by simulating a fragile API. I'll write a function that mimics a network request but throws an error if we try to run too many of them at once. I'll use a global counter to keep track of "active" requests.
const activeRequests = { count: 0 };
async function simulatedApiCall(id) {
activeRequests.count++;
// Our fake API crashes if more than 3 requests are active
if (activeRequests.count > 3) {
throw new Error(`Rate limit exceeded! Request ${id} failed.`);
}
console.log(`Processing request ${id}...`);
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
activeRequests.count--;
return `Data for ${id}`;
}
Now, here is where I usually make my first mistake when I'm rushing a prototype. I might try to just use a for...of loop with await inside it. That works, but it's incredibly slow because it's sequential. We don't want one-at-a-time; we want three-at-a-time.
If I try Promise.all(ids.map(id => simulatedApiCall(id))), it fails instantly because it launches all 1,000 requests simultaneously. We need a middle ground: a concurrency-limited queue.
Designing the worker pool
The most reliable way to handle this is to treat our tasks as a queue and create a set of "workers" that pull from that queue until it's empty. I like this approach because it separates the list of work from the execution logic.
First, I'll put all my IDs into an array that I can shift() from. Then, I'll create a worker function that keeps calling itself as long as there are tasks left.
async function runWithLimit(limit, items, taskFn) {
const queue = [...items];
const results = [];
async function worker() {
while (queue.length > 0) {
const item = queue.shift();
try {
const result = await taskFn(item);
results.push(result);
} catch (err) {
results.push(err); // Capture errors so the whole queue doesn't die
}
}
}
// Fire up the initial set of workers
const workers = Array.from({ length: limit }, worker);
await Promise.all(workers);
return results;
}
Correcting the results order
I just noticed a problem. Because the API calls take different amounts of time (thanks to that Math.random()), results.push(result) will store the data in the order they finished, not the order they were started. If the order of your data matters—which it usually does—this is a bug.
To fix this, I need to track the original index of the item. Instead of just shifting the value, I'll store the index and use that to place the result in the correct slot of a pre-allocated array.
async function runWithLimit(limit, items, taskFn) {
const queue = items.map((item, index) => ({ item, index }));
const results = new Array(items.length);
async function worker() {
while (queue.length > 0) {
const { item, index } = queue.shift();
try {
results[index] = await taskFn(item);
} catch (err) {
results[index] = err;
}
}
}
const workers = Array.from({ length: limit }, worker);
await Promise.all(workers);
return results;
}
Putting it all together
Now we have a robust utility. We can pass in our simulated API call, specify a limit of 3, and handle a huge list of IDs without triggering the rate limit or losing the order of our data.
const userIds = Array.from({ length: 20 }, (_, i) => i + 1);
runWithLimit(3, userIds, simulatedApiCall)
.then(data => console.log("All tasks finished:", data))
.catch(err => console.error("Fatal error:", err));
This pattern is the backbone of almost every professional scraping or data-migration script I've ever written. It gives you a "dial" to turn—if the API is feeling slow, you turn the limit down to 2; if they upgrade your plan, you bump it to 10.
📋 Practical Task
Build a Concurrent Image-Downloader Simulator
You are building a tool that simulates downloading a batch of high-resolution images. The "server" you are hitting has a strict limit: it can only handle 2 concurrent downloads. If a 3rd download starts before one of the first two finishes, the server crashes.
Requirements:
- Create a
simulateDownload(imageName)function that returns a promise. It should usesetTimeoutto resolve after a random time (between 500ms and 1500ms). - Implement a
downloadManager(images, limit)function. This function should take an array of image names (e.g.,['img1.jpg', 'img2.jpg', ...]) and a concurrency limit. - Ensure that no more than 2 downloads are happening at the same time.
- The final output must be an array of "Downloaded: [imageName]" strings, and they must remain in the same order as the original input array, regardless of which download finished first.
- Test your manager with a list of 10 images.
There are no comments for now.