Mastering Promises
A Promise is an object representing the future result of an asynchronous operation. It solves callback hell and allows you to write more declarative async code.
What is a Promise?
A Promise has three states:
pending → Initial state, no result yet
fulfilled → Operation succeeded, result value available
rejected → Operation failed, error available
The state changes only once and cannot be reversed (pending → fulfilled or pending → rejected).
// Visualizing Promise states
const p1 = new Promise((resolve, reject) => {
// pending state...
setTimeout(() => resolve('Success!'), 1000);
// fulfilled state after 1 second
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Failure!')), 1000);
// rejected state after 1 second
});
console.log(p1); // Promise { <pending> }
Creating a Promise
// Basic creation
const promise = new Promise((resolve, reject) => {
// Perform async operation
const success = Math.random() > 0.5;
if (success) {
resolve('Operation succeeded!'); // Transition to fulfilled state
} else {
reject(new Error('Operation failed!')); // Transition to rejected state
}
});
// Practical example: wrapping an HTTP request
function fetchUser(id) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/users/${id}`);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
// Timer Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log('Start');
await delay(2000);
console.log('After 2 seconds');
}
.then(), .catch(), .finally()
.then() - Handling Success
fetchUser(1)
.then(user => {
console.log(user.name);
return user.id; // Passed to the next .then
})
.then(id => {
console.log('ID:', id);
});
// .then(onFulfilled, onRejected) - errors can also be handled via the second argument
fetchUser(1).then(
user => console.log('Success:', user),
err => console.error('Failure:', err) // Similar to .catch but with a difference
);
.catch() - Handling Errors
fetchUser(999)
.then(user => processUser(user))
.catch(err => {
// Catches errors thrown anywhere in the chain
if (err.message.includes('404')) {
console.log('User not found');
} else {
console.error('Unexpected error:', err);
}
});
// Same as .then(null, handler) but .catch is recommended
.finally() - Always Runs
// Runs regardless of success or failure (useful for cleanup)
showLoadingSpinner();
fetchData('/api/large-dataset')
.then(data => displayData(data))
.catch(err => showErrorMessage(err))
.finally(() => {
hideLoadingSpinner(); // Always runs
// finally does not change the value
});
// finally does not affect the Promise value
Promise.resolve(42)
.finally(() => console.log('Cleanup')) // Prints 'Cleanup'
.then(val => console.log(val)); // Prints 42 (value preserved)
Promise Chaining
// Each .then() returns a new Promise
const result = fetchUser(1)
.then(user => {
console.log('User:', user.name);
return fetch(`/api/orders?userId=${user.id}`); // Returns a Promise
})
.then(response => response.json()) // Automatically flattens the Promise
.then(orders => {
console.log('Order count:', orders.length);
return orders.filter(o => o.status === 'active');
})
.then(activeOrders => {
console.log('Active orders:', activeOrders);
return activeOrders; // Final result
})
.catch(err => {
console.error('Error:', err);
return []; // Return default value to continue the chain
});
Static Methods
Promise.resolve() / Promise.reject()
// Wrapping an existing value in a Promise
const p1 = Promise.resolve(42);
p1.then(val => console.log(val)); // 42
// Creating an immediate rejection
const p2 = Promise.reject(new Error('Immediate failure'));
p2.catch(err => console.error(err.message)); // Immediate failure
// Useful for normalizing function return types
function getConfig(useCache) {
if (useCache && cachedConfig) {
return Promise.resolve(cachedConfig); // Wrap sync value in Promise
}
return fetchConfig(); // Async
}
Promise.all() - Wait for All to Complete
// Succeeds only if all Promises succeed. Fails immediately if any one fails
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Parallel execution improves performance
console.time('sequential');
const a = await fetch('/api/a').then(r => r.json());
const b = await fetch('/api/b').then(r => r.json());
console.timeEnd('sequential'); // ~200ms (serial)
console.time('parallel');
const [c, d] = await Promise.all([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json())
]);
console.timeEnd('parallel'); // ~100ms (parallel)
// Behavior on failure
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('Failure!')),
Promise.resolve(3)
]).catch(err => console.error(err.message)); // 'Failure!' - others ignored
Promise.race() - First One to Complete
// Returns the result of whichever Promise settles first (success or failure)
const fastest = await Promise.race([
fetch('/api/server1').then(r => r.json()),
fetch('/api/server2').then(r => r.json()),
fetch('/api/server3').then(r => r.json())
]);
// Implementing a timeout pattern
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout: exceeded ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
try {
const data = await withTimeout(fetchLargeData(), 5000);
console.log(data);
} catch (err) {
console.error(err.message); // Timeout error if it takes more than 5 seconds
}
Promise.allSettled() - All Complete (Regardless of Success/Failure)
// Collects results of all Promises (continues even if some fail)
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999), // 404 error
fetchUser(2)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${index}: Success -`, result.value);
} else {
console.log(`${index}: Failure -`, result.reason.message);
}
});
// In practice: collecting results from multiple API calls
async function fetchMultipleUsers(ids) {
const results = await Promise.allSettled(
ids.map(id => fetchUser(id))
);
return {
successful: results
.filter(r => r.status === 'fulfilled')
.map(r => r.value),
failed: results
.filter(r => r.status === 'rejected')
.map(r => r.reason)
};
}
Promise.any() - First One to Succeed
// Returns the first success. Throws AggregateError if all fail
try {
const result = await Promise.any([
fetch('/api/primary').then(r => r.json()),
fetch('/api/backup1').then(r => r.json()),
fetch('/api/backup2').then(r => r.json())
]);
console.log('First success:', result);
} catch (err) {
if (err instanceof AggregateError) {
console.error('All requests failed:', err.errors);
}
}
// Difference between Promise.race and Promise.any
// race: first settled (success or failure)
// any: first fulfilled (success only)
Practical Examples
Parallel API Call Pattern
// Load all user dashboard data at once
async function loadDashboard(userId) {
const [user, stats, notifications, recentActivity] = await Promise.all([
api.getUser(userId),
api.getUserStats(userId),
api.getNotifications(userId),
api.getRecentActivity(userId)
]);
return {
user,
stats,
notifications,
recentActivity
};
}
// Continue even if some fail
async function loadDashboardRobust(userId) {
const results = await Promise.allSettled([
api.getUser(userId),
api.getUserStats(userId),
api.getNotifications(userId),
api.getRecentActivity(userId)
]);
const [userResult, statsResult, notifResult, activityResult] = results;
return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
stats: statsResult.status === 'fulfilled' ? statsResult.value : {},
notifications: notifResult.status === 'fulfilled' ? notifResult.value : [],
recentActivity: activityResult.status === 'fulfilled' ? activityResult.value : []
};
}
Retry Pattern
function retry(fn, attempts = 3, delay = 1000) {
return new Promise((resolve, reject) => {
function attempt(remaining) {
fn()
.then(resolve)
.catch(err => {
if (remaining <= 1) {
reject(err);
} else {
console.log(`Retrying... remaining attempts: ${remaining - 1}`);
setTimeout(() => attempt(remaining - 1), delay);
}
});
}
attempt(attempts);
});
}
// Usage
retry(() => fetchUnstableAPI(), 3, 2000)
.then(data => console.log('Success:', data))
.catch(err => console.error('Failed after 3 attempts:', err));
Building a Pipeline with Promise Chaining
// Data processing pipeline
function processImage(imageUrl) {
return fetch(imageUrl)
.then(response => {
if (!response.ok) throw new Error('Image load failed');
return response.blob();
})
.then(blob => createImageBitmap(blob))
.then(bitmap => {
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
return canvas;
})
.then(canvas => canvas.toDataURL('image/webp', 0.8))
.catch(err => {
console.error('Image processing failed:', err);
return '/images/placeholder.webp'; // Fallback
});
}
Promise Anti-Patterns
// Bad example 1: Unnecessary wrapping in the Promise constructor (Promise Constructor Anti-pattern)
// Bad
function fetchData() {
return new Promise((resolve, reject) => {
fetch('/api/data')
.then(r => r.json())
.then(resolve)
.catch(reject);
});
}
// Good - if it already returns a Promise, just return it directly
function fetchData() {
return fetch('/api/data').then(r => r.json());
}
// Bad example 2: Forgetting .catch()
async function riskyOperation() {
fetch('/api/data')
.then(r => r.json())
.then(processData);
// No .catch() → errors are silently ignored (Unhandled Rejection)
}
// Bad example 3: Using Promise without await
async function missingAwait() {
const result = fetchData(); // No await!
console.log(result); // Prints Promise { <pending> }
}
Pro Tips
1. Promise.withResolvers() (ES2024)
// Old approach: inconvenient access to resolve/reject from outside
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ES2024: Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers();
// Practical example
function createDeferred() {
return Promise.withResolvers();
}
const { promise: userPromise, resolve: resolveUser } = createDeferred();
// Can resolve from somewhere else later
setTimeout(() => resolveUser({ name: 'Alice' }), 1000);
const user = await userPromise;
2. Preserving access to values in Promise chaining
// When you need to access intermediate values later
async function withMultipleValues() {
// Approach 1: async/await
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(user.name, orders.length); // Both accessible
// Approach 2: pass as an object
return getUser(1)
.then(user => getOrders(user.id).then(orders => ({ user, orders })))
.then(({ user, orders }) => console.log(user.name, orders.length));
}
3. Concurrency Limiting
// Process only N items at a time
async function batchProcess(items, processor, concurrency = 5) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
}
return results;
}
// Process 100 items in parallel batches of 5
const processed = await batchProcess(
Array.from({ length: 100 }, (_, i) => i),
async (item) => {
await delay(100);
return item * 2;
},
5
);