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:
- JavaScript Error Handling Patterns Cheat Sheet — 45+ patterns for try/catch, Promise errors, custom errors, and global handlers
- JavaScript Array Methods Cheat Sheet — 40+ array methods with mutating vs non-mutating labels
- JavaScript String Methods Cheat Sheet — Complete string manipulation reference
- JavaScript ES2024+ Features Cheat Sheet — Modern JavaScript APIs including Promise.withResolvers, Object.groupBy, and iterator helpers
- JavaScript Date/Time Methods Cheat Sheet — Date parsing, formatting, and manipulation
- Node.js Built-in Modules Cheat Sheet — 70+ Node.js APIs across fs, path, http, events, stream, buffer, crypto, os, and more
- React Hooks Cheat Sheet — useEffect, useState, and custom hook patterns
- React Performance Patterns Cheat Sheet — Memoization, lazy loading, and optimization
- TypeScript Utility Types Cheat Sheet — Built-in mapped types and conditional type patterns
- Web APIs Cheat Sheet — 60+ browser APIs for storage, network, media, sensors, and more
- CSS Animation Properties Cheat Sheet — Keyframes, transitions, timing functions, and performance
- Bash Scripting Cheat Sheet — Variables, conditionals, loops, and one-liners
- SQL Advanced Patterns Cheat Sheet — Window functions, CTEs, recursive queries, and optimization