TL;DR — Quick Summary
Promises are the foundation of asynchronous JavaScript. A Promise has three states: pending, fulfilled, and rejected. Use async/await for readable async code; Promise.all for parallel execution; try/catch or .catch() for error handling; Promise.race for timeouts; and Promise.allSettled when you need all results even if some fail. This guide covers everything from basics to advanced patterns with real code examples.
Key Takeaways
- A Promise is a single-use container for an async result — its state is immutable once settled
- async/await is syntactic sugar over Promises — they are functionally identical
- Promise.all fails fast; Promise.allSettled waits for all and never rejects
- Errors must be explicitly caught with .catch() or try/catch — or they become unhandled rejections
- Parallel execution with Promise.all is dramatically faster than sequential awaits
- Promise.race enables timeout wrappers and fastest-source patterns
- Avoid forgetting await, swallowing errors, and nesting Promises (the Promise equivalent of callback hell)
1. Promise Basics: States and Lifecycle
Before Promises, JavaScript handled asynchronous operations through nested callback functions — a pattern notorious for producing deeply indented, hard-to-maintain "callback hell." ES6 (2015) introduced Promises as a structured way to represent the eventual completion (or failure) of an asynchronous operation and its resulting value.
A Promise object represents an asynchronous operation that has exactly three mutually exclusive states:
- Pending — The initial state. The operation has not yet completed.
- Fulfilled — The operation completed successfully and the Promise has a resolved value.
- Rejected — The operation failed and the Promise has a rejection reason (typically an Error object).
Once a Promise transitions from pending to either fulfilled or rejected, it is settled — its state and value are permanently fixed. This immutability is a core guarantee of the Promise specification.
// The three Promise states
const pending = new Promise(() => {}); // Never settles
const fulfilled = Promise.resolve('success'); // Immediately fulfilled
const rejected = Promise.reject(new Error('fail')); // Immediately rejected
// Handle results with .then() and .catch()
fulfilled
.then(value => console.log('Value:', value)) // Value: success
.catch(error => console.error('Error:', error));
rejected
.then(value => console.log('Never runs'))
.catch(error => console.error('Caught:', error.message)); // Caught: fail
// .finally() runs regardless of outcome
fetch('/api/data')
.then(res => res.json())
.catch(err => ({ error: err.message }))
.finally(() => hideLoadingSpinner()); // Always executes.then(), .catch(), and .finally()
The three core Promise handler methods cover every outcome scenario. Each returns a new Promise, enabling the fluent chaining pattern:
// .then(onFulfilled, onRejected) — both params are optional
promise.then(
value => console.log('Fulfilled with:', value),
error => console.log('Rejected with:', error) // rarely used
);
// Best practice: use .catch() instead of the second .then() arg
// .catch(onRejected) is shorthand for .then(undefined, onRejected)
fetch('/api/users')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(users => {
renderUserList(users);
return users.length; // Pass a value to the next .then()
})
.then(count => console.log(`Rendered ${count} users`))
.catch(error => {
// Catches ALL errors from the entire chain above
console.error('Failed to load users:', error.message);
return []; // Recover: return default value to continue the chain
})
.finally(() => {
// Cleanup: runs whether fulfilled or rejected
setLoading(false);
});2. Creating Promises: new Promise(), resolve, and reject
You create a Promise with the new Promise(executor) constructor. The executor function receives two parameters: resolve (call when the operation succeeds) and reject (call when it fails). The executor runs synchronously — the Promise body executes immediately.
// Basic Promise constructor
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000); // Pauses for 1 second
// Promise with a value
function delayedValue(value, ms) {
return new Promise(resolve =>
setTimeout(() => resolve(value), ms)
);
}
const result = await delayedValue('hello', 500); // 'hello' after 500ms
// Wrapping callback-based APIs
function readFileAsync(path, encoding = 'utf8') {
return new Promise((resolve, reject) => {
require('fs').readFile(path, encoding, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Simulating an async operation with random failure
function unstableRequest() {
return new Promise((resolve, reject) => {
const willSucceed = Math.random() > 0.3;
setTimeout(() => {
if (willSucceed) resolve({ status: 'ok', data: [1, 2, 3] });
else reject(new Error('Request failed: server unavailable'));
}, Math.random() * 1000);
});
}
// Shorthand static methods
const immediate = Promise.resolve({ id: 1, name: 'Alice' });
const failure = Promise.reject(new TypeError('Expected string'));
// Always catch rejected promises or they become unhandled rejections
failure.catch(() => {});3. Promise Chaining: Error Propagation and Return Values
Because .then() always returns a new Promise, you can chain operations in a flat, readable sequence. A value returned from a .then() handler becomes the resolved value of the next Promise in the chain. An error thrown in any step propagates through the chain to the nearest .catch().
// Correct: flat chain (not nested)
fetchUser(userId)
.then(user => fetchUserPosts(user.id)) // Returns a Promise
.then(posts => fetchComments(posts[0].id)) // Waits for posts first
.then(comments => renderComments(comments))
.catch(err => showErrorMessage(err));
// Error propagation: errors skip .then() handlers
Promise.resolve('start')
.then(val => { throw new Error('step 2 failed'); })
.then(val => console.log('skipped'))
.then(val => console.log('also skipped'))
.catch(err => console.log('Caught:', err.message)); // Caught: step 2 failed
// Recovering in .catch() — the chain continues after catching
fetchPrimary()
.catch(err => {
console.warn('Primary source failed, using backup:', err.message);
return fetchBackup(); // Return backup Promise — chain resumes
})
.then(data => processData(data)); // Runs with backup data
// Passing multiple values through the chain
fetchOrderById(orderId)
.then(order => {
return Promise.all([
Promise.resolve(order), // Pass through the original
fetchCustomer(order.customerId), // Fetch related data in parallel
]);
})
.then(([order, customer]) => generateInvoice(order, customer))
.then(invoice => sendEmail(invoice))
.catch(err => logError('Invoice generation failed', err));4. async/await: The Modern Async Syntax
Introduced in ES2017, async/await is syntactic sugar that makes Promise-based code read like synchronous code. An async function always returns a Promise. The await keyword pauses execution of the async function until the awaited Promise settles, then resumes with the resolved value.
// async function always returns a Promise
async function greet(name) {
return `Hello, ${name}!`; // Auto-wrapped in Promise.resolve(...)
}
greet('Alice').then(console.log); // Hello, Alice!
// await pauses execution until the Promise settles
async function loadDashboard(userId) {
const user = await fetchUser(userId); // Waits here
const settings = await fetchSettings(user.id); // Then waits here
const stats = await fetchStats(user.id); // Then waits here
return { user, settings, stats };
}
// Error handling with try/catch
async function safeLoadDashboard(userId) {
try {
const user = await fetchUser(userId);
const data = await fetchUserData(user.id);
return renderDashboard(user, data);
} catch (error) {
if (error.status === 404) {
return renderNotFound();
}
console.error('Dashboard load failed:', error);
throw error; // Re-throw errors you cannot handle here
} finally {
hideLoadingIndicator(); // Always runs
}
}
// Top-level await (ES2022, in ES Modules)
const config = await loadConfig('./app.config.json');
console.log('App version:', config.version);Sequential vs. Parallel Execution — The Critical Performance Difference
The most common async/await performance mistake is using sequential awaits for independent operations. Each sequential await adds its wait time to the total, while parallel execution only takes as long as the slowest operation.
// SLOW: Sequential — total time = 300 + 200 + 400 = 900ms
async function slowApproach() {
const users = await fetchUsers(); // Waits 300ms
const products = await fetchProducts(); // Then waits 200ms more
const orders = await fetchOrders(); // Then waits 400ms more
return { users, products, orders };
}
// FAST: Parallel — total time ≈ max(300, 200, 400) = 400ms
async function fastApproach() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders(),
]);
return { users, products, orders };
}
// Parallel array processing (not sequential!)
async function processAllItems(items) {
// Correct: start all at once, wait for all to finish
return Promise.all(items.map(item => processItem(item)));
}
// Common mistake: for...of with await is sequential
async function sequentialByMistake(items) {
const results = [];
for (const item of items) {
results.push(await processItem(item)); // Processes one at a time!
}
return results;
}5. Promise Combinators: all, allSettled, race, any
JavaScript provides four static combinator methods for coordinating multiple Promises, each with a different resolution strategy suited to different use cases:
| Method | Resolves when | Rejects when | Best use case |
|---|---|---|---|
| Promise.all | ALL fulfill | ANY single rejects (fail-fast) | All results required; one failure invalidates all |
| Promise.allSettled | ALL settle (any status) | Never rejects | Batch ops; need all statuses including failures |
| Promise.race | FIRST settles (any status) | FIRST rejects | Timeout wrapper; first-response wins |
| Promise.any | FIRST fulfills | ALL reject (AggregateError) | Multiple fallback sources; first success wins |
// Promise.all — all or nothing
try {
const [user, posts, friends] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchFriends(id),
]);
renderProfile(user, posts, friends);
} catch (error) {
console.error('Profile load failed:', error.message);
}
// Promise.allSettled — batch operations with partial failure tolerance
const imageUploads = urls.map(url => uploadImage(url));
const outcomes = await Promise.allSettled(imageUploads);
let succeeded = 0, failed = 0;
outcomes.forEach((outcome, index) => {
if (outcome.status === 'fulfilled') {
console.log(`Image ${index}: uploaded`);
succeeded++;
} else {
console.warn(`Image ${index}: failed — ${outcome.reason.message}`);
failed++;
}
});
console.log(`${succeeded} uploaded, ${failed} failed`);
// Promise.race — take the first settled result (including rejections)
const firstCache = await Promise.race([
fetchFromRedis(key),
fetchFromMemcached(key),
]);
// Promise.any — first success, ignores individual rejections
try {
const data = await Promise.any([
fetchFromPrimaryDB(),
fetchFromReplicaDB(),
fetchFromCacheFallback(),
]);
console.log('Got data from fastest source:', data);
} catch (aggregateError) {
// Thrown only when ALL promises reject
aggregateError.errors.forEach((err, i) =>
console.error(`Source ${i + 1}: ${err.message}`)
);
}6. Error Handling Patterns
Robust error handling is where many async codebases fall short. Unhandled Promise rejections crash Node.js processes (since v15) and trigger warnings in browsers. Here are the essential patterns for handling async errors correctly.
// Custom error classes for better categorization
class NetworkError extends Error {
constructor(message, statusCode, endpoint) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
this.endpoint = endpoint;
}
}
class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.value = value;
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
}
}
// Differentiated error handling with instanceof
async function updateUserProfile(userId, data) {
try {
validateProfileData(data); // Throws ValidationError
const user = await fetchUser(userId); // Throws NetworkError or NotFoundError
return await saveUser({ ...user, ...data });
} catch (error) {
if (error instanceof ValidationError) {
return { success: false, error: 'validation', field: error.field };
}
if (error instanceof NotFoundError) {
return { success: false, error: 'not_found' };
}
if (error instanceof NetworkError && error.statusCode >= 500) {
throw error; // Retry-able — let the caller handle
}
throw error; // Unknown error — re-throw for upper layers
}
}
// Global handlers (last resort)
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
process.exit(1); // Recommended in Node.js 15+
});
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled Promise rejection:', event.reason);
reportToMonitoring(event.reason);
event.preventDefault();
});7. Advanced Patterns
Promise.withResolvers() — ES2024
Promise.withResolvers() (Node.js 22+, Chrome 119+) returns an object containing a new Promise together with its resolve and reject functions — eliminating the need to capture them through the constructor callback.
// Old "deferred" pattern — verbose variable capture
let externalResolve, externalReject;
const deferred = new Promise((res, rej) => {
externalResolve = res;
externalReject = rej;
});
// New: Promise.withResolvers() — clean and explicit
const { promise, resolve, reject } = Promise.withResolvers();
// Practical use: event-to-promise bridge
function waitForEvent(emitter, event, errorEvent = 'error') {
const { promise, resolve, reject } = Promise.withResolvers();
const onSuccess = data => { emitter.off(errorEvent, onError); resolve(data); };
const onError = err => { emitter.off(event, onSuccess); reject(err); };
emitter.once(event, onSuccess);
emitter.once(errorEvent, onError);
return promise;
}
const socket = createWebSocket('wss://api.example.com');
await waitForEvent(socket, 'open');
console.log('WebSocket connected!');
// Polyfill for older environments
if (!Promise.withResolvers) {
Promise.withResolvers = function() {
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
};
}Promisifying Callback-Based APIs
const { promisify } = require('util');
const fs = require('fs');
const dns = require('dns');
// Node.js util.promisify for standard (err, result) callbacks
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const dnsLookup = promisify(dns.lookup);
const config = JSON.parse(await readFile('./config.json', 'utf8'));
const { address } = await dnsLookup('example.com');
// Manual promisify utility for any callback-style function
function promisifyFn(fn, context = null) {
return function promisified(...args) {
return new Promise((resolve, reject) => {
fn.call(context, ...args, (err, ...results) => {
if (err) return reject(err);
resolve(results.length <= 1 ? results[0] : results);
});
});
};
}
// Modern Node.js: use fs/promises (already promisified)
import { readFile, writeFile, readdir } from 'fs/promises';
const content = await readFile('./data.txt', 'utf8');
const fileList = await readdir('./src');
await writeFile('./output.txt', content.toUpperCase());Bounded Concurrency: Processing Large Datasets in Parallel
// Process items with a maximum concurrency limit
async function withBoundedConcurrency(items, processItem, concurrency = 5) {
const results = new Array(items.length);
const inFlight = new Set();
for (let i = 0; i < items.length; i++) {
const index = i;
const promise = processItem(items[i]).then(result => {
results[index] = result;
inFlight.delete(promise);
});
inFlight.add(promise);
if (inFlight.size >= concurrency) {
await Promise.race(inFlight); // Wait for a slot to open
}
}
await Promise.all(inFlight); // Wait for remaining in-flight
return results;
}
// Usage: process 1000 items, 5 at a time
const userIds = Array.from({ length: 1000 }, (_, i) => i + 1);
const profiles = await withBoundedConcurrency(
userIds,
id => fetch(`/api/users/${id}`).then(r => r.json()),
5 // Max 5 concurrent requests
);8. Real-World Examples
Fetch with Retry Logic and Exponential Backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(10000), // 10s timeout per attempt
});
// Don't retry client errors (4xx) except 429 (rate limit)
if (!response.ok) {
if (response.status < 500 && response.status !== 429) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (attempt < maxRetries) {
// Respect Retry-After header or use exponential backoff
const retryAfter = response.headers.get('Retry-After');
const waitMs = retryAfter
? parseInt(retryAfter) * 1000
: baseDelay * Math.pow(2, attempt - 1);
console.warn(`Attempt ${attempt}/${maxRetries}: HTTP ${response.status}, retrying in ${waitMs}ms`);
await new Promise(resolve => setTimeout(resolve, waitMs));
continue;
}
throw new Error(`HTTP ${response.status} after ${maxRetries} attempts`);
}
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') throw new Error('Request timed out');
if (attempt === maxRetries) throw error;
const waitMs = baseDelay * Math.pow(2, attempt - 1);
console.warn(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
}
}Timeout Wrapper with AbortController
// Generic Promise timeout (works with any Promise)
function withTimeout(promise, ms, message) {
const timeout = new Promise((_, reject) =>
setTimeout(
() => reject(new Error(message ?? `Operation timed out after ${ms}ms`)),
ms
)
);
return Promise.race([promise, timeout]);
}
// Better for fetch: AbortController actually cancels the request
async function fetchWithAbort(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId); // Always clean up the timer
}
}
// Usage
try {
const data = await fetchWithAbort('https://api.example.com/slow-endpoint', 3000);
renderData(data);
} catch (error) {
showError(error.message);
}Concurrent Dashboard Loading
async function loadDashboard(userId) {
// Start independent requests immediately (before any await)
const [userPromise, settingsPromise] = [
fetchUser(userId),
fetchSettings(userId),
];
// Wait for user first (needed to get user-dependent IDs)
const user = await userPromise;
// Now fetch all user-dependent data in parallel
const [posts, followers, notifications, settings] = await Promise.all([
fetchPosts(user.id, { limit: 10 }),
fetchFollowers(user.id),
fetchNotifications(user.id, { unreadOnly: true }),
settingsPromise, // Already in flight — just awaiting its result
]);
return {
user,
posts,
followers,
notifications,
settings,
unreadCount: notifications.filter(n => !n.read).length,
};
}9. Common Mistakes and How to Fix Them
Mistake 1: Forgetting await
// WRONG: data is a Promise object, not the actual data
async function wrong() {
const data = fetchUser(1); // Missing await! data = Promise{<pending>}
console.log(data.name); // undefined — Promise has no .name
if (data) { ... } // Always true — Promise objects are truthy
}
// RIGHT: await the Promise to get the resolved value
async function correct() {
const data = await fetchUser(1); // data = { id: 1, name: 'Alice', ... }
console.log(data.name); // 'Alice'
}
// SUBTLE: forgetting await in a conditional
async function sneakyBug(userId) {
const user = fetchUserIfExists(userId); // Missing await!
if (user) { // Always true! Promise is truthy
await sendWelcomeEmail(user); // user is a Promise, not a user object
}
}
// Use ESLint rule @typescript-eslint/no-floating-promises to catch theseMistake 2: Swallowing Errors (Empty catch blocks)
// WRONG: errors are silently discarded — impossible to debug
async function silentFailure() {
try {
await doImportantWork();
} catch (e) {
// Empty catch: error gone forever. Network error? Bug? Nobody knows.
}
}
// RIGHT: always log at minimum, or re-throw
async function properErrorHandling() {
try {
await doImportantWork();
} catch (error) {
console.error('doImportantWork failed:', error);
// Option A: Re-throw so the caller can handle it
throw error;
// Option B: Return a fallback (only if failure is genuinely acceptable)
// return fallbackValue;
}
}Mistake 3: Nested Promises (Promise Callback Hell)
// WRONG: nested .then() — Promise callback hell
fetchUser(id).then(user => {
fetchPosts(user.id).then(posts => { // Nested!
fetchComments(posts[0].id).then(comments => { // Even deeper!
renderPage(user, posts, comments);
});
});
});
// RIGHT: flat async/await
async function loadPage(id) {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id); // Depends on user
const comments = await fetchComments(posts[0].id); // Depends on posts
return renderPage(user, posts, comments);
}Mistake 4: Wrapping a Promise in new Promise() (Anti-Pattern)
// WRONG: wrapping an already-Promise-based API unnecessarily
function fetchDataWrong(url) {
return new Promise((resolve, reject) => {
fetch(url) // fetch() already returns a Promise!
.then(res => res.json())
.then(resolve) // Redundant
.catch(reject); // Error handling is also double-handled
});
}
// RIGHT: just chain or use async/await directly
function fetchDataRight(url) {
return fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
});
}
async function fetchDataAsync(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}10. Callbacks vs. Promises vs. async/await: Comparison Table
| Aspect | Callbacks | Promises | async/await |
|---|---|---|---|
| Readability | Poor (nesting) | Good (chaining) | Excellent (sync-like) |
| Error handling | Easy to miss | .catch() chain | try/catch blocks |
| Stack traces | Truncated | Partial | Full and clear |
| Debugging (breakpoints) | Difficult | Moderate | Excellent |
| Parallel execution | Manual counters | Promise.all / .race | await Promise.all |
| Conditional async flow | Complex nesting | Moderate | Simple (if/else, loops) |
| TypeScript support | Weak | Good | Best (full type inference) |
| Browser support | Universal | ES6+ (IE with polyfill) | ES2017+ (Babel for older) |
| Learning curve | Low initially | Medium | Low (if Promises understood) |
| Best for | Legacy codebases | Composing/chaining | Most new async code |
Frequently Asked Questions
What are the three states of a JavaScript Promise?
A Promise has three states: pending (initial — not yet settled), fulfilled (operation completed successfully with a value), and rejected (operation failed with a reason/error). Once settled (fulfilled or rejected), a Promise's state is permanent and never changes. You handle fulfilled Promises with .then() and rejected ones with .catch().
What is the difference between async/await and .then()/.catch()?
Both are built on the same Promise mechanism. async/await uses synchronous-style syntax making code easier to read, debug, and reason about — especially for sequential operations. Error handling uses familiar try/catch. .then()/.catch() chains are more explicit about the async nature and useful for dynamic composition. async/await compiles to .then()/.catch() under the hood, so choose based on readability. Most modern codebases prefer async/await.
What is the difference between Promise.all and Promise.allSettled?
Promise.all fails fast — if any one promise rejects, the whole Promise.all rejects immediately with that error. Use it when all results are required. Promise.allSettled waits for every promise to settle regardless of outcome and returns an array of { status, value/reason } objects. It never rejects. Use it for batch operations where you want all results, including failures.
How do I run multiple async operations in parallel?
Use Promise.all with an array of started promises: const [a, b] = await Promise.all([fetchA(), fetchB()]). The key is that fetchA() and fetchB() are invoked (started) before any await — this kicks off both operations simultaneously. Never use sequential awaits like: const a = await fetchA(); const b = await fetchB(); for independent operations — that runs them one at a time.
How do I add a timeout to a Promise?
Use Promise.race with a timeout Promise: function withTimeout(p, ms) { return Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))]); }. For fetch specifically, use AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), ms); await fetch(url, { signal: ctrl.signal }); — this properly cancels the underlying request rather than just abandoning it.
What happens if I forget await before a Promise?
Without await, you receive the Promise object itself rather than its resolved value. The variable will be a Promise, not your data. Comparisons like if (result) are always true (Promises are objects). The Promise's errors will not be caught by surrounding try/catch blocks. TypeScript and ESLint (with the @typescript-eslint/no-floating-promises rule) can catch many forgotten-await bugs at compile time.
What is promisification?
Promisification converts callback-style APIs (where the last argument is a callback(error, result) function) into Promise-returning functions. Node.js provides util.promisify(fn) for standard Node-style callbacks. You can also promisify manually: return new Promise((resolve, reject) => fn(...args, (err, result) => err ? reject(err) : resolve(result))). Node.js also provides fs/promises, dns/promises, and other built-in Promise APIs.
What is Promise.withResolvers() and when is it useful?
Promise.withResolvers() (ES2024) returns { promise, resolve, reject } — letting you access the resolver functions outside the constructor callback. It replaces the "deferred" pattern where you capture resolve/reject via closure. It is especially useful for converting event emitters to Promises, building cancellation tokens, and any case where you need to resolve/reject a Promise from a different scope or callback.
Conclusion
JavaScript Promises and async/await are indispensable tools for modern web development. Mastering them means understanding Promise states, choosing the right combinator (Promise.all vs Promise.allSettled vs Promise.race), handling errors explicitly at every level, and knowing when to run operations in parallel vs. sequentially. Avoid the common pitfalls — forgetting await, swallowing errors in empty catch blocks, and nesting Promises unnecessarily. With these patterns in hand, your async code will be reliable, performant, and easy to maintain. Use our JSON Formatter and JWT Decoder to debug the API responses you fetch with these async patterns.