JavaScript Async Patterns - Complete Guide

Asynchronous programming is essential for modern JavaScript. This guide covers async patterns, from callbacks to async/await, and best practices for handling asynchronous operations.

Callbacks #

Traditional approach to async programming.

function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = () => {
    if (xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error('Request failed'));
    }
  };
  xhr.send();
}

// Usage
fetchData('/api/users', (error, data) => {
  if (error) {
    console.error(error);
    return;
  }
  console.log(data);
});

Callback Hell #

// Nested callbacks - hard to read
fetchUser((err, user) => {
  if (err) return handleError(err);

  fetchPosts(user.id, (err, posts) => {
    if (err) return handleError(err);

    fetchComments(posts[0].id, (err, comments) => {
      if (err) return handleError(err);

      console.log(comments);
    });
  });
});

Promises #

Better abstraction for async operations.

function fetchData(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error('Request failed'));
      }
    };
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.send();
  });
}

// Usage
fetchData('/api/users')
  .then(data => console.log(data))
  .catch(error => console.error(error));

Promise Chaining #

fetchUser()
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

Promise.all() #

Run promises in parallel.

Promise.all([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
])
  .then(users => {
    console.log('All users:', users);
  })
  .catch(error => {
    console.error('At least one failed:', error);
  });

Promise.allSettled() #

Wait for all promises, regardless of outcome.

Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
])
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`User ${index + 1}:`, result.value);
      } else {
        console.error(`User ${index + 1} failed:`, result.reason);
      }
    });
  });

Promise.race() #

First promise to settle wins.

Promise.race([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3()
])
  .then(data => {
    console.log('Fastest server:', data);
  })
  .catch(error => {
    console.error('All failed:', error);
  });

Promise.any() #

First promise to fulfill wins.

Promise.any([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3()
])
  .then(data => {
    console.log('First successful:', data);
  })
  .catch(error => {
    console.error('All rejected:', error);
  });

Async/Await #

Syntactic sugar over promises.

async function fetchUserData() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return comments;
  } catch (error) {
    console.error('Error:', error);
  }
}

// Usage
fetchUserData().then(comments => {
  console.log(comments);
});

Error Handling #

async function fetchData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch:', error);
    throw error;
  }
}

Parallel Execution #

// Sequential - Slow
async function sequential() {
  const user1 = await fetchUser(1);  // Wait
  const user2 = await fetchUser(2);  // Wait
  const user3 = await fetchUser(3);  // Wait
  return [user1, user2, user3];
}

// Parallel - Fast
async function parallel() {
  const [user1, user2, user3] = await Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);
  return [user1, user2, user3];
}

Advanced Patterns #

Retry Logic #

async function retry(fn, maxAttempts = 3, delay = 1000) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }

      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await sleep(delay);
      delay *= 2;  // Exponential backoff
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
const data = await retry(() => fetchData('/api/users'));

Timeout #

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

// Usage
try {
  const data = await timeout(fetchData('/api/slow'), 5000);
  console.log(data);
} catch (error) {
  console.error('Request timed out');
}

Rate Limiting #

class RateLimiter {
  constructor(maxConcurrent) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
  }

  async run(fn) {
    while (this.running >= this.maxConcurrent) {
      await new Promise(resolve => this.queue.push(resolve));
    }

    this.running++;

    try {
      return await fn();
    } finally {
      this.running--;
      const resolve = this.queue.shift();
      if (resolve) resolve();
    }
  }
}

// Usage
const limiter = new RateLimiter(3);  // Max 3 concurrent

const requests = urls.map(url =>
  limiter.run(() => fetch(url))
);

const results = await Promise.all(requests);

Queue #

class AsyncQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  async add(fn) {
    this.queue.push(fn);

    if (!this.processing) {
      await this.process();
    }
  }

  async process() {
    this.processing = true;

    while (this.queue.length > 0) {
      const fn = this.queue.shift();

      try {
        await fn();
      } catch (error) {
        console.error('Task failed:', error);
      }
    }

    this.processing = false;
  }
}

// Usage
const queue = new AsyncQueue();

queue.add(async () => {
  console.log('Task 1');
  await sleep(1000);
});

queue.add(async () => {
  console.log('Task 2');
  await sleep(500);
});

Debounce Async #

function debounceAsync(fn, delay) {
  let timeoutId;
  let latestResolve;
  let latestReject;

  return function (...args) {
    clearTimeout(timeoutId);

    return new Promise((resolve, reject) => {
      latestResolve = resolve;
      latestReject = reject;

      timeoutId = setTimeout(async () => {
        try {
          const result = await fn.apply(this, args);
          latestResolve(result);
        } catch (error) {
          latestReject(error);
        }
      }, delay);
    });
  };
}

// Usage
const debouncedSearch = debounceAsync(async (query) => {
  const results = await fetch(`/api/search?q=${query}`);
  return results.json();
}, 300);

// Only last call executes
debouncedSearch('java');
debouncedSearch('javascript');  // This one executes

Memoization with Async #

function memoizeAsync(fn) {
  const cache = new Map();
  const pending = new Map();

  return async function (...args) {
    const key = JSON.stringify(args);

    // Return cached result
    if (cache.has(key)) {
      return cache.get(key);
    }

    // Return pending promise
    if (pending.has(key)) {
      return pending.get(key);
    }

    // Execute and cache
    const promise = fn.apply(this, args);
    pending.set(key, promise);

    try {
      const result = await promise;
      cache.set(key, result);
      return result;
    } finally {
      pending.delete(key);
    }
  };
}

// Usage
const fetchUser = memoizeAsync(async (id) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
});

// First call - hits API
const user1 = await fetchUser(1);

// Second call - returns cached
const user2 = await fetchUser(1);

Parallel Limit #

async function parallelLimit(tasks, limit) {
  const results = [];
  const executing = [];

  for (const [index, task] of tasks.entries()) {
    const promise = Promise.resolve().then(() => task());

    results[index] = promise;

    if (limit <= tasks.length) {
      const e = promise.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);

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

  return Promise.all(results);
}

// Usage
const tasks = urls.map(url => () => fetch(url));
const results = await parallelLimit(tasks, 5);  // Max 5 concurrent

Cancel Token #

class CancelToken {
  constructor() {
    this.cancelled = false;
    this.promise = new Promise((resolve) => {
      this.cancel = () => {
        this.cancelled = true;
        resolve();
      };
    });
  }

  throwIfCancelled() {
    if (this.cancelled) {
      throw new Error('Operation cancelled');
    }
  }
}

async function fetchWithCancel(url, cancelToken) {
  cancelToken.throwIfCancelled();

  const response = await fetch(url);

  cancelToken.throwIfCancelled();

  const data = await response.json();

  return data;
}

// Usage
const token = new CancelToken();

setTimeout(() => token.cancel(), 1000);

try {
  const data = await fetchWithCancel('/api/slow', token);
  console.log(data);
} catch (error) {
  console.log('Cancelled');
}

AbortController #

const controller = new AbortController();
const { signal } = controller;

// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('/api/data', { signal });
  const data = await response.json();
  console.log(data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request aborted');
  }
}

Async Iterators #

Async Generator #

async function* fetchPages(url) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();

    yield data.items;

    hasMore = data.hasMore;
    page++;
  }
}

// Usage
for await (const items of fetchPages('/api/items')) {
  console.log('Page items:', items);
}

Async Iterable #

class AsyncDataSource {
  constructor(url) {
    this.url = url;
  }

  async *[Symbol.asyncIterator]() {
    let page = 1;

    while (true) {
      const response = await fetch(`${this.url}?page=${page}`);
      const data = await response.json();

      if (data.items.length === 0) break;

      for (const item of data.items) {
        yield item;
      }

      page++;
    }
  }
}

// Usage
const dataSource = new AsyncDataSource('/api/users');

for await (const user of dataSource) {
  console.log(user);
}

Event Emitters #

Custom Event Emitter #

class AsyncEventEmitter {
  constructor() {
    this.events = new Map();
  }

  on(event, listener) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event).push(listener);
  }

  async emit(event, data) {
    const listeners = this.events.get(event) || [];

    for (const listener of listeners) {
      await listener(data);
    }
  }

  off(event, listener) {
    const listeners = this.events.get(event);

    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
  }
}

// Usage
const emitter = new AsyncEventEmitter();

emitter.on('data', async (data) => {
  await processData(data);
});

await emitter.emit('data', { id: 1 });

Observables #

Simple Observable #

class Observable {
  constructor(subscriber) {
    this.subscriber = subscriber;
  }

  subscribe(observer) {
    return this.subscriber(observer);
  }

  static from(iterable) {
    return new Observable((observer) => {
      for (const value of iterable) {
        observer.next(value);
      }
      observer.complete();
    });
  }

  map(fn) {
    return new Observable((observer) => {
      return this.subscribe({
        next: (value) => observer.next(fn(value)),
        error: (err) => observer.error(err),
        complete: () => observer.complete()
      });
    });
  }
}

// Usage
const numbers = Observable.from([1, 2, 3, 4, 5]);

const doubled = numbers.map(x => x * 2);

doubled.subscribe({
  next: (value) => console.log(value),
  complete: () => console.log('Done')
});

Best Practices #

Always Handle Errors #

// Bad
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// Good
async function fetchData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

Don’t Await Unnecessarily #

// Bad - Unnecessary await
async function getUser(id) {
  return await fetchUser(id);
}

// Good - Return promise directly
async function getUser(id) {
  return fetchUser(id);
}

// Only await if you need the value
async function getUserName(id) {
  const user = await fetchUser(id);
  return user.name;
}

Use Promise.all for Independence #

// Bad - Sequential
async function getData() {
  const users = await fetchUsers();
  const posts = await fetchPosts();
  return { users, posts };
}

// Good - Parallel
async function getData() {
  const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
  ]);
  return { users, posts };
}

Avoid Async in Constructors #

// Bad
class User {
  constructor(id) {
    this.data = await fetchUser(id);  // Can't use await here
  }
}

// Good
class User {
  constructor(data) {
    this.data = data;
  }

  static async create(id) {
    const data = await fetchUser(id);
    return new User(data);
  }
}

// Usage
const user = await User.create(1);

Summary #

  1. Prefer async/await - More readable than promises
  2. Handle all errors - Use try/catch
  3. Parallel when possible - Use Promise.all
  4. Add timeouts - Prevent hanging
  5. Implement retries - For unreliable operations
  6. Rate limit - Avoid overwhelming APIs
  7. Cancel when needed - Use AbortController
  8. Test async code - Use proper test frameworks

Async patterns are essential for modern JavaScript. Master these techniques to write efficient, maintainable asynchronous code.