Tutorial JavaScript Async Promises Developer Tools Cheat Sheet

Free JavaScript Promise & Async Patterns Cheat Sheet Online — Interactive Reference for Developers

· 22 min read

Modern JavaScript is asynchronous by default. Every network request, every file read, every database query, every user interaction event flows through the same single-threaded runtime. The language that once ran simple form validations in Netscape Navigator now powers serverless backends, real-time data pipelines, and streaming media applications. Yet the fundamental contract remains unchanged: JavaScript executes one thing at a time, and everything else waits in line. Understanding javascript promises, js async await, and the javascript event loop is no longer optional for professional developers. It is the price of admission.

The transition from callback-driven code to javascript async patterns was one of the most significant shifts in the language's history. Before Promises, developers suffered through javascript callback hell — nested anonymous functions, error handling scattered across layers, and code that read backwards. Promises introduced a structured way to represent future values. Async/await made those structures readable. But the abstractions only work when you understand the primitives beneath them: the javascript microtask queue, the javascript macrotask scheduler, and the event loop that coordinates them both.

That is why we built the free interactive JavaScript Promise & Async Patterns Cheat Sheet. It is designed with a distinctive deep-ocean aesthetic — a submarine cable relay station visual theme with deep ocean-blue-black backgrounds, signal-green and amber status indicators, coral data-flow accents, and bioluminescent pulse animations that trace active signal paths. Every entry sits on a brushed metal panel card, organized by system category, searchable in real time, and copy-ready with one click. Whether you are debugging a race condition, designing a concurrent data pipeline, or preparing for a senior engineering interview, every pattern is at your fingertips. Everything runs client-side in your browser — no data ever leaves your machine.

Skip ahead: If you want to explore the tool while reading, open the JavaScript Promise & Async Patterns Cheat Sheet in another tab. Use it to test the code examples in this article in real time.

Why Promises Matter: From Callback Hell to Structured Concurrency

Before Promises, asynchronous JavaScript looked like a pyramid of doom. Each nested callback added another layer of indentation. Error handling required duplicate logic at every level. Returning values from deep inside a callback chain was nearly impossible. This was javascript callback hell, and every developer who wrote Node.js before 2015 experienced it.

Promises solved this by introducing a first-class representation of a future value. A Promise is an object that may produce a single value some time in the future, or a reason why it cannot. It has exactly three states: pending, fulfilled, or rejected. Once settled, a Promise never changes state. This immutability makes Promises predictable and composable.

Async/await, introduced in ES2017, was the final piece of the puzzle. It allowed developers to write asynchronous code that looked synchronous, without sacrificing the non-blocking behavior that makes JavaScript efficient. But the syntactic sugar only works if you understand what happens under the hood. An await expression yields control back to the event loop, allowing other tasks to run. When the awaited Promise settles, the async function resumes as a javascript microtask. This subtle behavior explains why the order of console.log statements in async code sometimes surprises even experienced developers.

Understanding these primitives matters for performance and reliability. Misusing javascript promise all can overload a server with thousands of concurrent requests. Ignoring the difference between microtasks and macrotasks can cause UI jank. Not knowing how to implement a javascript retry pattern means your application fails permanently on the first transient network error. The patterns in this article are not academic exercises. They are the tools that separate fragile scripts from production-grade systems.

Promise Basics: Creating, Chaining, and Settling

Every Promise begins with the Promise constructor, which takes an executor function with resolve and reject parameters. The executor runs immediately and synchronously. Any errors thrown inside the executor automatically reject the Promise.

Creating Promises

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Data loaded');
  }, 1000);
});

// Rejection
const fail = new Promise((resolve, reject) => {
  reject(new Error('Something went wrong'));
});

The .then() method attaches fulfillment and rejection handlers. It returns a new Promise, enabling chaining. The .catch() method is syntactic sugar for .then(null, onRejected). The .finally() method runs regardless of outcome, making it ideal for cleanup.

Chaining

fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error('Chain failed:', error))
  .finally(() => console.log('Done'));

A critical detail: if a handler returns a value, the next .then() receives it. If a handler returns a Promise, the chain waits for it to settle. If a handler throws, the next .catch() receives the error. This error propagation mechanism is what makes Promise chains composable.

Implicit Promise Creation

Async functions automatically wrap their return value in a Promise. Even returning a plain value from an async function produces a fulfilled Promise.

async function getNumber() {
  return 42; // Implicitly returns Promise.resolve(42)
}

getNumber().then(n => console.log(n)); // 42

Promise.resolve and Promise.reject

These static methods create immediately settled Promises. They are useful for normalizing values into the Promise chain and for writing test doubles.

Promise.resolve(42).then(n => console.log(n));
Promise.reject(new Error('Oops')).catch(e => console.error(e));

// Resolving a thenable
const thenable = {
  then(resolve) { resolve('from thenable'); }
};
Promise.resolve(thenable).then(v => console.log(v));

Async/Await: Readable Asynchronous Code

The async keyword declares a function that returns a Promise. The await keyword pauses execution of the async function until the awaited Promise settles, then resumes with the resolved value. If the Promise rejects, await throws the rejection reason.

Basic Syntax

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user;
}

Error Handling with Try/Catch

Because await throws on rejection, you can use standard try/catch blocks for async error handling. This is one of the biggest ergonomic wins of async/await.

async function loadUserSafe(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error('Failed to load user:', error);
    return null;
  }
}

Top-Level Await

ES2022 introduced javascript top level await, allowing await at the module level without wrapping it in an async function. This simplifies module initialization and dynamic imports.

// module.js
const data = await fetch('/api/config').then(r => r.json());
export default data;

// Dynamic import with top-level await
const { helper } = await import('./helper.js');

Note that top-level await blocks module execution. The importing module waits until all top-level awaits complete before evaluating its own body.

Awaiting in Loops: Sequential vs Parallel

One of the most common mistakes in async JavaScript is using forEach with async callbacks. forEach does not wait for Promises. Use for...of for sequential execution and map + Promise.all for parallel execution.

// BAD: forEach does not await
urls.forEach(async url => {
  const data = await fetch(url);
  console.log(data);
});

// GOOD: sequential with for...of
for (const url of urls) {
  const data = await fetch(url);
  console.log(data);
}

// GOOD: parallel with map + Promise.all
const results = await Promise.all(
  urls.map(url => fetch(url))
);

Implicit Returns and Await

Remember that return promise and return await promise behave differently inside try/catch blocks. Without await, the try block succeeds immediately by returning the Promise object. If that Promise later rejects, the catch block never runs.

// BAD: catch never fires
async function bad() {
  try {
    return fetch('/api'); // Returns promise, does not await
  } catch (error) {
    // Never reached for fetch failures
  }
}

// GOOD: await inside try
async function good() {
  try {
    return await fetch('/api');
  } catch (error) {
    console.error('Caught:', error);
  }
}

Promise Static Methods: The Concurrency Toolkit

JavaScript provides five static methods for combining and racing Promises. Each has distinct semantics for rejection, fulfillment, and settling behavior. Choosing the wrong one is a common source of bugs.

Method Input Rejects When Use Case
Promise.all Iterable of promises First rejection; abandons rest All must succeed; fail fast
Promise.allSettled Iterable of promises Never rejects Need all results; handle partial failure
Promise.race Iterable of promises First settlement (resolve or reject) Timeouts, first response wins
Promise.any Iterable of promises All reject (AggregateError) First success wins; redundant requests
Promise.withResolvers None (returns {promise, resolve, reject}) Controlled by caller Deferred resolution, external control

Promise.all

javascript promise all waits for all promises to fulfill and returns an array of their values. It rejects immediately when any promise rejects, and the remaining promises are abandoned — their results are lost.

const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json()),
]);

Promise.allSettled

javascript promise allsettled waits for every promise to settle, regardless of outcome. It never rejects. The result is an array of objects with status and either value or reason.

const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c'),
]);

const successes = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

const failures = results
  .filter(r => r.status === 'rejected')
  .map(r => r.reason);

Promise.race

javascript promise race settles as soon as any input promise settles, whether fulfilled or rejected. It is the standard pattern for adding timeouts.

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('Timeout')), 5000)
);

const data = await Promise.race([fetch('/api/slow'), timeout]);

Promise.any

javascript promise any returns the first fulfilled promise. It only rejects if every input promise rejects, in which case it throws an AggregateError containing all rejection reasons.

try {
  const fastest = await Promise.any([
    fetchFromCDN1(),
    fetchFromCDN2(),
    fetchFromCDN3(),
  ]);
  return fastest;
} catch (aggregateError) {
  console.error('All CDNs failed:', aggregateError.errors);
}

Promise.withResolvers (ES2024)

Introduced in ES2024, Promise.withResolvers() returns an object with promise, resolve, and reject properties. This is useful when you need to resolve a Promise from outside its constructor.

const { promise, resolve, reject } = Promise.withResolvers();

// Later, from any scope:
resolve('Done');

await promise; // 'Done'

Error Handling in Async Code

Asynchronous error handling in JavaScript has multiple layers: local try/catch, Promise.catch chaining, global unhandledrejection handlers, and custom error types. Understanding when to use each layer is essential for robust applications.

Try/Catch with Async/Await

The most natural pattern for async error handling. Remember to await inside the try block, or the catch will not fire.

async function fetchData() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error; // Re-throw if caller should handle it
  }
}

.catch Chaining

For Promise chains, .catch() handles any rejection in the chain above it. You can recover from errors by returning a value from catch.

fetch('/api/data')
  .then(r => r.json())
  .catch(error => {
    console.error(error);
    return { fallback: true }; // Recovery value
  })
  .then(data => render(data));

Unhandled Rejection

Promises that reject without a handler trigger the unhandledrejection event. Every production application should listen for this.

window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled rejection:', event.reason);
  logToService(event.reason);
  event.preventDefault();
});

AggregateError

When javascript promise any rejects because all promises failed, it throws an AggregateError. The .errors property contains all individual rejection reasons.

try {
  await Promise.any([p1, p2, p3]);
} catch (error) {
  if (error instanceof AggregateError) {
    error.errors.forEach(e => console.error(e.message));
  }
}

Retry Pattern with Exponential Backoff

Transient failures are normal in distributed systems. A javascript retry pattern with exponential backoff prevents hammering a struggling service.

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      const res = await fetch(url, options);
      if (res.ok) return res;
      if (res.status < 500) throw new Error(`Client error ${res.status}`);
    } catch (error) {
      if (i === maxRetries) throw error;
    }
    const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
    await new Promise(r => setTimeout(r, delay));
  }
}

Concurrent Control Patterns

JavaScript's single-threaded nature does not mean you cannot control concurrency. These patterns manage how many operations run simultaneously, how tasks are ordered, and how resources are shared.

Parallel with Map + Promise.all

The simplest way to run independent operations concurrently. Be careful not to overwhelm external services.

const urls = ['/api/a', '/api/b', '/api/c'];
const results = await Promise.all(
  urls.map(url => fetch(url).then(r => r.json()))
);

Sequential with for...of

When operations depend on each other or when you must rate-limit requests, iterate sequentially.

for (const url of urls) {
  const data = await fetch(url).then(r => r.json());
  await process(data); // Each waits for the previous
}

Waterfall

A javascript waterfall passes the result of each step to the next. This is common in data transformation pipelines.

const result = await step1()
  .then(data => step2(data))
  .then(data => step3(data))
  .then(data => step4(data));

Task Queue

A javascript queue processes tasks with a concurrency limit. Essential for batch operations against rate-limited APIs.

async function runQueue(tasks, concurrency = 3) {
  const results = [];
  const executing = [];

  for (const [index, task] of tasks.entries()) {
    const promise = task().then(result => ({ index, result }));
    results.push(promise);

    if (tasks.length >= concurrency) {
      executing.push(promise);
      if (executing.length >= concurrency) {
        await Promise.race(executing);
        executing.splice(executing.findIndex(p => p === promise), 1);
      }
    }
  }

  return Promise.all(results);
}

Semaphore

A javascript semaphore limits the number of concurrent accesses to a shared resource.

class Semaphore {
  constructor(max) {
    this.max = max;
    this.count = 0;
    this.queue = [];
  }

  async acquire() {
    if (this.count < this.max) {
      this.count++;
      return;
    }
    await new Promise(resolve => this.queue.push(resolve));
    this.count++;
  }

  release() {
    this.count--;
    if (this.queue.length > 0) {
      const next = this.queue.shift();
      next();
    }
  }
}

Mutex

A javascript mutex is a semaphore with max concurrency of 1. It ensures mutual exclusion for critical sections.

class Mutex {
  constructor() {
    this.promise = Promise.resolve();
  }

  async acquire() {
    let release;
    const newPromise = new Promise(resolve => { release = resolve; });
    const wait = this.promise.then(() => release);
    this.promise = this.promise.then(() => newPromise);
    await wait;
    return release;
  }
}

// Usage:
const mutex = new Mutex();
async function criticalSection() {
  const release = await mutex.acquire();
  try {
    // Only one execution at a time
  } finally {
    release();
  }
}

Debounce and Throttle

javascript debounce throttle patterns control how often async operations execute in response to rapid events like scrolling or typing.

// Debounce: execute after pause
function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

// Throttle: execute at most once per period
function throttle(fn, ms) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= ms) {
      last = now;
      fn(...args);
    }
  };
}

Memoize with Promises

Cache async results to avoid redundant network requests or expensive computations.

function memoizeAsync(fn) {
  const cache = new Map();
  return async (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const promise = fn(...args);
    cache.set(key, promise);
    return promise;
  };
}

Stream Processing: Async Generators and ReadableStream

Not all data arrives at once. Streams process data incrementally, reducing memory usage and improving perceived performance. JavaScript supports streams through javascript readablestream, async generators, and javascript for await loops.

ReadableStream

The Streams API provides a standard way to handle streaming data. A javascript readablestream reads chunks incrementally.

const response = await fetch('/api/large-file');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  processChunk(value);
}

Async Generators

javascript generator async functions yield values asynchronously. They are ideal for paginated APIs and event streams.

async function* fetchPages(url) {
  let nextUrl = url;
  while (nextUrl) {
    const res = await fetch(nextUrl);
    const data = await res.json();
    yield data.results;
    nextUrl = data.next;
  }
}

// Consume with for await...of
for await (const page of fetchPages('/api/items')) {
  console.log(`Received ${page.length} items`);
}

For Await...Of

javascript for await consumes async iterables, including async generators and ReadableStreams.

async function* generateNumbers() {
  for (let i = 0; i < 5; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

for await (const num of generateNumbers()) {
  console.log(num); // 0, 1, 2, 3, 4 with delays
}

Pipeline Pattern

Chain async generators to create data processing pipelines. Each stage transforms the stream independently.

async function* filter(iterable, predicate) {
  for await (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

async function* map(iterable, transform) {
  for await (const item of iterable) {
    yield transform(item);
  }
}

const pipeline = map(
  filter(fetchPages('/api/items'), item => item.active),
  item => item.name
);

for await (const name of pipeline) {
  console.log(name);
}

Cancellation: AbortController and Timeout Patterns

Long-running async operations need to be cancellable. The javascript abortcontroller API provides a standard mechanism for cancellation that works with fetch, streams, and custom async code.

AbortController Basics

Create a controller, pass its signal to cancellable operations, then call abort() to cancel.

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
  .then(r => r.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request cancelled');
    }
  });

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

Fetch Cancellation

Passing an AbortSignal to fetch() causes the request to abort when the signal triggers. This works for both request and response phases.

const controller = new AbortController();

try {
  const res = await fetch('/api/slow', { signal: controller.signal });
  const data = await res.json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Fetch was aborted');
  }
}

Timeout with Promise.race

The classic javascript timeout pattern races the operation against a timer.

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

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

AbortSignal.timeout

Modern browsers support AbortSignal.timeout(ms), which creates a signal that automatically aborts after the specified duration.

const res = await fetch('/api/data', {
  signal: AbortSignal.timeout(5000)
});

// Combine with user cancellation
const userController = new AbortController();
const res = await fetch('/api/data', {
  signal: AbortSignal.any([
    userController.signal,
    AbortSignal.timeout(10000)
  ])
});

Real-World Recipes

These patterns combine the primitives into robust, production-ready solutions for common async challenges.

Robust Fetch Wrapper

A production-grade fetch wrapper handles HTTP errors, timeouts, retries, and JSON parsing in one place.

async function robustFetch(url, options = {}) {
  const { timeout = 10000, retries = 2, ...fetchOptions } = options;

  for (let i = 0; i <= retries; i++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const res = await fetch(url, {
        ...fetchOptions,
        signal: controller.signal,
      });
      clearTimeout(timeoutId);

      if (!res.ok) {
        throw new Error(`HTTP ${res.status}: ${res.statusText}`);
      }
      return await res.json();
    } catch (error) {
      clearTimeout(timeoutId);
      if (i === retries) throw error;
      if (error.name === 'AbortError') throw error;
      await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
    }
  }
}

Polling

javascript polling repeatedly checks a condition until it is met or a timeout expires.

async function poll(fn, interval, timeout) {
  const endTime = Date.now() + timeout;

  while (Date.now() < endTime) {
    const result = await fn();
    if (result) return result;
    await new Promise(r => setTimeout(r, interval));
  }

  throw new Error('Polling timed out');
}

// Usage: poll until job is complete
const job = await poll(
  () => checkJobStatus(jobId),
  2000,  // Check every 2 seconds
  60000  // Give up after 60 seconds
);

Retry with Exponential Backoff and Jitter

Add random jitter to backoff delays to prevent thundering herd problems when a service recovers.

async function retryWithJitter(fn, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries) throw error;
      const baseDelay = Math.pow(2, i) * 1000;
      const jitter = Math.random() * 1000;
      await new Promise(r => setTimeout(r, baseDelay + jitter));
    }
  }
}

Lazy Initialization with Promise Memoization

Ensure an expensive initialization runs exactly once, even if multiple callers request it simultaneously.

class LazyLoader {
  constructor(factory) {
    this.factory = factory;
    this.promise = null;
  }

  get() {
    if (!this.promise) {
      this.promise = this.factory().catch(err => {
        this.promise = null; // Allow retry on failure
        throw err;
      });
    }
    return this.promise;
  }
}

const db = new LazyLoader(() => connectToDatabase());

// All callers get the same promise
const conn1 = await db.get();
const conn2 = await db.get();

Circuit Breaker

Prevent cascading failures by stopping requests to a failing service temporarily.

class CircuitBreaker {
  constructor(fn, options = {}) {
    this.fn = fn;
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failures = 0;
  }

  async execute(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.resetTimeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await this.fn(...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }
}

Promise Pool / Concurrency Limit

Limit the number of concurrent promises when processing large batches.

async function promisePool(tasks, concurrency) {
  const results = [];
  const executing = [];

  for (const [index, task] of tasks.entries()) {
    const promise = task().then(result => {
      results[index] = result;
    });
    executing.push(promise);

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

  await Promise.all(executing);
  return results;
}

// Usage: process 100 URLs with max 5 concurrent
const tasks = urls.map(url => () => fetch(url));
const results = await promisePool(tasks, 5);

The Interactive Cheat Sheet

Reading about async patterns is valuable, but having a searchable reference at your fingertips is transformative. Our free interactive JavaScript Promise & Async Patterns Cheat Sheet includes all 60+ patterns from this article, organized into filterable categories with the distinctive deep-ocean submarine cable relay station aesthetic.

Every entry features real-time search, category filtering by system function, one-click copy for all code examples, and syntax highlighting. The visual design uses deep ocean-blue-black backgrounds with signal-green, amber, and coral accents on brushed metal panel cards. Bioluminescent pulse animations trace active signal paths, making the experience feel like operating a real submarine relay station console.

The cheat sheet runs entirely in your browser. No account required. No data collection. No server round-trips. Just open it and start searching for javascript promise all, js async await, javascript event loop, or any other pattern you need.

Related Tools and References

Async patterns are just one part of writing robust JavaScript. Explore these related tools in the DevToolkit collection:

Found this useful? Check out our free developer tools or browse more articles.