Frontend Labs
⋯

17. Mini project: an async task queue with concurrency limits and retry

class TaskQueue {
  #concurrency; #running = 0; #queue = [];
  constructor(concurrency = 3) { this.#concurrency = concurrency; }

  add(taskFn, { retries = 2 } = {}) {
    return new Promise((resolve, reject) => {
      this.#queue.push({ taskFn, retries, resolve, reject });
      this.#next();
    });
  }

  async #next() {
    if (this.#running >= this.#concurrency || this.#queue.length === 0) return;
    this.#running++;
    const item = this.#queue.shift();
    try {
      resolve_result: {
        try {
          const result = await item.taskFn();
          item.resolve(result);
          break resolve_result;
        } catch (err) {
          if (item.retries > 0) {
            this.#queue.push({ ...item, retries: item.retries - 1 });
          } else {
            item.reject(err);
          }
        }
      }
    } finally {
      this.#running--;
      this.#next();   // pull the next queued item, whether we just succeeded or freed a slot
    }
  }
}

const queue = new TaskQueue(2);   // only 2 fetches in flight at once
const urls = ["/api/a", "/api/b", "/api/c", "/api/d", "/api/e"];
const results = await Promise.all(
  urls.map((url) => queue.add(() => fetch(url).then((r) => r.json())))
);

This is the exact shape of pattern real apps use to avoid saturating a browser's per-origin connection limit (historically 6 for HTTP/1.1, see §26) when firing many requests at once.


Generated from real-life-JS-guide.md · 8 Oct 2026 16:36