JavaScript Promises: The Deep Dive You Need
Promises are everywhere in JS. Understand them deeply.
What Is a Promise?
// A promise is a placeholder for a future value
const promise = fetch('/api/data'); // Returns a promise immediately
// It has 3 states:
// 1. Pending: Initial state, waiting to resolve or reject
// 2. Fulfilled: Completed successfully (has a value)
// 3. Rejected: Failed (has a reason/error)
// Once settled → immutable (can't change again)
Creating Promises
// Basic constructor
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000); // Waits 1 second
// With error handling
function randomSuccess() {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
resolve('Success!');
} else {
reject(new Error('Random failure'));
}
});
}
// Wrapping callback-based APIs
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
Chaining
fetch('/api/user')
.then(res => res.json()) // Transform response
.then(user => user.posts) // Extract posts
.then(posts => posts.filter(p => p.published)) // Filter
.then(posts => render(posts)) // Use result
.catch(err => showError(err)); // Handle ANY error in the chain
// Each .then() returns a NEW promise
// Errors propagate down to the nearest .catch()
Error Handling Gotchas
// ❌ Error in .then() is caught by .catch()
Promise.resolve(1)
.then(() => { throw new Error('Oops') })
.then(() => console.log("Won't run"))
.catch(err => console.error("Caught:", err.message)); // "Caught: Oops"
// ❌ .catch() returns a resolved promise (error is "handled")
Promise.reject(new Error('Fail'))
.catch(err => {
console.log('Error handled:', err.message);
return 'fallback'; // This becomes the value for next .then()
})
.then(val => console.log(val)); // "fallback" — continues!
// ✅ Re-throw to keep rejecting
Promise.reject(new Error('Fail'))
.catch(err => {
console.log('Logging:', err.message);
throw err; // Re-throw — keeps rejection going
})
.then(val => console.log("Won't run"))
.catch(err => console.error("Final catch:", err.message));
// ⚠️ Unhandled rejection = crash!
const p = Promise.reject(new Error('No catch!'));
// In Node.js: UnhandledRejection warning
// In browser: Console warning + unhandledrejection event
Promise Combinators
const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
const p3 = fetch('/api/comments');
// All must succeed
const [users, posts, comments] = await Promise.all([p1, p2, p3]);
// If ANY fails → throws immediately (others continue but results lost)
// Wait for all (success or fail)
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
if (r.status === 'fulfilled') useData(r.value);
else logError(r.reason);
});
// First to resolve wins (ignores rejections)
const first = await Promise.race([
fetchFastServer(),
timeout(3000), // Fallback if too slow
]);
// First SUCCESS wins (ignores failures)
const data = await Promise.any([
fetchPrimary(),
fetchBackup1(),
fetchBackup2(),
]);
Advanced Patterns
Promise Caching (Memoization)
class PromiseCache {
#cache = new Map();
get(key, factory) {
if (this.#cache.has(key)) {
return this.#cache.get(key); // Return existing promise
}
const promise = factory().finally(() => {
// Optional: auto-expire after time
setTimeout(() => this.#cache.delete(key), 60_000);
});
this.#cache.set(key, promise);
return promise;
}
clear() { this.#cache.clear(); }
}
const userCache = new PromiseCache();
// Multiple calls before resolved = only ONE actual request
const user1 = await userCache.get(123, () => fetchUser(123));
const user2 = await userCache.get(123, () => fetchUser(123)); // Same promise!
Request Coalescing
class RequestCoalescer {
#pending = new Map();
async request(key, fn) {
if (this.#pending.has(key)) {
return this.#pending.get(key); // Join existing request
}
const promise = fn().finally(() => this.#pending.delete(key));
this.#pending.set(key, promise);
return promise;
}
}
const coalescer = new RequestCoalescer();
// 100 components call this simultaneously:
// Only 1 API call made, all get the same result
const config = await coalescer.request('app-config', () => fetchConfig());
Retry as a Promise
```function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
return new Promise((resolve, reject) => {
let attempt = 0;
const tryAgain = () => {
fn()
.then(resolve)
.catch(error => {
attempt++;
if (attempt >= maxRetries) return reject(error);
const delay = baseDelay * Math.pow(2, attempt - 1);
setTimeout(tryAgain, delay);
});
};
tryAgain();
});
}
// Usage
const data = await retryWithBackoff(() => fetch('/api/data'), 5);```
Timeout Wrapper
function withTimeout(promise, ms, message = 'Timeout') {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${message} after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetch('/api/data'), 5000);
Debugging Promises
// Track unresolved promises
const pendingPromises = new Set();
function track(promise, label) {
pendingPromises.add({ promise, label, created: Date.now() });
promise.finally(() => {
pendingPromises.delete(...pendingPromises.find(p => p.promise === promise));
});
return promise;
}
// Check for leaks periodically
setInterval(() => {
const stale = [...pendingPromises].filter(
p => Date.now() - p.created > 30_000
);
if (stale.length > 0) {
console.warn('Stale promises:', stale.map(p => p.label));
}
}, 10_000);
What's your favorite Promise pattern?
Follow @armorbreak for more JavaScript content.
Top comments (0)