Skip to content

JavaScript Concurrency

Understanding async programming in JavaScript and TypeScript.

The JavaScript Model

JavaScript is single-threaded but non-blocking.

Main Thread:  ─execute─execute─[await]─execute─[await]─execute─
                               │        │
                               └──I/O───┘  (handled by runtime)

All JavaScript code runs on one thread. Long-running operations (network, timers) are handled outside the main thread by the runtime.

Core Mechanisms

Mechanism Purpose
Event Loop Schedules and executes code
Promises Represent future values
async/await Clean syntax for promises
Web Workers True parallelism (separate threads)

Quick Comparison

Python JavaScript
asyncio Event Loop (built-in)
await await
asyncio.gather() Promise.all()
multiprocessing Web Workers
ThreadPoolExecutor No direct equivalent

Topics

Topic Description
Event Loop How JavaScript handles async
Promises & Async/Await Modern async patterns
Web Workers True parallelism
React Concurrent Features Concurrent rendering

Quick Examples

Parallel Fetches

// Fetch multiple resources concurrently
async function fetchUserData(userId) {
  const [user, posts, comments] = await Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/posts?userId=${userId}`).then(r => r.json()),
    fetch(`/api/comments?userId=${userId}`).then(r => r.json()),
  ]);

  return { user, posts, comments };
}

Sequential Operations

// When order matters
async function processItems(items) {
  const results = [];
  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }
  return results;
}

Rate-Limited Concurrency

// Process with limited parallelism
async function processWithLimit(items, limit, fn) {
  const results = [];
  const executing = [];

  for (const item of items) {
    const promise = fn(item).then(result => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });

    results.push(promise);
    executing.push(promise);

    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

// Usage
await processWithLimit(urls, 5, fetchUrl);

Background Processing with Web Worker

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ data: largeArray, operation: 'sort' });

worker.onmessage = (event) => {
  console.log('Sorted:', event.data);
};

// worker.js
self.onmessage = (event) => {
  const { data, operation } = event.data;
  if (operation === 'sort') {
    const sorted = data.sort((a, b) => a - b);
    self.postMessage(sorted);
  }
};

Common Patterns

Error Handling

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
    }
  }
}

Timeout

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

// Usage
const result = await withTimeout(fetch('/api/data'), 5000);

Debounce

function debounce(fn, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Usage
const debouncedSearch = debounce(search, 300);
input.addEventListener('input', (e) => debouncedSearch(e.target.value));

Throttle

function throttle(fn, limit) {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage
const throttledScroll = throttle(handleScroll, 100);
window.addEventListener('scroll', throttledScroll);

When to Use What

Is it CPU-intensive?
├── Yes → Web Worker
└── No → Is it waiting for something?
    ├── Yes → async/await
    └── No → Regular synchronous code
Situation Solution
API calls async/await with fetch
Multiple API calls Promise.all()
Heavy computation Web Worker
UI updates during computation requestAnimationFrame + chunking
React long renders Concurrent features

Best Practices

  1. Use async/await — Cleaner than .then() chains
  2. Use Promise.all() — For independent parallel operations
  3. Handle errors — Always catch promise rejections
  4. Avoid blocking — Keep main thread free for UI
  5. Use Web Workers — For CPU-intensive tasks
  6. Consider React concurrent — For smooth UI updates

Common Mistakes

// Bad: Sequential when parallel is possible
async function bad() {
  const user = await fetchUser();      // Wait...
  const posts = await fetchPosts();    // Then wait...
  const comments = await fetchComments(); // Then wait...
}

// Good: Parallel
async function good() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);
}

// Bad: forEach with async (doesn't wait)
items.forEach(async (item) => {
  await processItem(item);  // Doesn't wait!
});

// Good: for...of waits
for (const item of items) {
  await processItem(item);  // Actually waits
}

// Or if parallel is OK:
await Promise.all(items.map(item => processItem(item)));