Now that you understand the basics of Promises and chaining, it's time to explore advanced patterns, edge cases, and production-ready techniques. This tutorial covers everything from anti-patterns to sophisticated async flows that you'll encounter in real-world applications.
Pending → Fulfilled (value)
↓
Pending → Rejected (reason)
Once settled, a Promise's state is immutable. You cannot change a fulfilled promise to rejected or vice versa.
const promise = new Promise((resolve, reject) => {
resolve("Success!");
reject(new Error("Too late!")); // Ignored — promise already fulfilled
});
promise.then(value => console.log(value)); // "Success!"The executor function passed to new Promise() runs synchronously and immediately:
console.log("Before promise");
new Promise((resolve) => {
console.log("Inside executor"); // Runs immediately!
resolve("done");
});
console.log("After promise");
// Output:
// Before promise
// Inside executor
// After promiseThe
.then()callback, however, runs asynchronously as a microtask.
new Promise((resolve, reject) => {
throw new Error("Executor error");
}).catch(error => {
console.error(error.message); // "Executor error"
});Promise.resolve("ok")
.then(value => {
throw new Error("Then error");
})
.catch(error => {
console.error(error.message); // "Then error"
});class NetworkError extends Error {}
class ValidationError extends Error {}
fetchData()
.catch(error => {
if (error instanceof NetworkError) {
console.error("Network issue:", error.message);
return retryFetch();
}
if (error instanceof ValidationError) {
console.error("Invalid data:", error.message);
return { data: [] };
}
throw error; // Re-throw unexpected errors
});// ❌ Wrapping a Promise in another Promise
function badFetch(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => resolve(response))
.catch(error => reject(error));
});
}
// ✅ Just return the Promise directly
function goodFetch(url) {
return fetch(url);
}
// ❌ Same with async functions
async function badFetch2(url) {
return new Promise((resolve, reject) => {
fetch(url).then(resolve).catch(reject);
});
}
// ✅ async already returns a Promise
async function goodFetch2(url) {
return fetch(url);
}// ❌ Missing return breaks the chain
function processUser(userId) {
return fetchUser(userId)
.then(user => {
fetchOrders(user.id); // Not returned!
})
.then(orders => {
console.log(orders); // undefined
});
}
// ✅ Return every Promise
function processUser(userId) {
return fetchUser(userId)
.then(user => fetchOrders(user.id))
.then(orders => {
console.log(orders);
return orders;
});
}// ❌ Mixing paradigms
function mixedApproach(userId, callback) {
fetchUser(userId)
.then(user => {
callback(null, user); // Still using callbacks!
})
.catch(error => {
callback(error);
});
}
// ✅ Return a Promise
function purePromise(userId) {
return fetchUser(userId);
}// ❌ Still nested!
fetchUser(1)
.then(user => {
return fetchOrders(user.id).then(orders => {
return fetchProducts(orders[0].id).then(products => {
console.log(products);
});
});
});
// ✅ Flat chain
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchProducts(orders[0].id))
.then(products => console.log(products));const tasks = [
() => fetchUser(1),
() => fetchOrders(1),
() => fetchProducts(1)
];
const results = await tasks.reduce(
(promise, task) => promise.then(results =>
task().then(result => [...results, result])
),
Promise.resolve([])
);async function mapWithConcurrency(items, mapper, concurrency) {
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(mapper));
results.push(...batchResults);
}
return results;
}
// Fetch max 5 URLs at a time
const pages = await mapWithConcurrency(urls, fetchPage, 5);function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetchData(), 5000);async function retry(fn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const waitTime = delay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed. Retrying in ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
// Usage
const data = await retry(() => fetchData(), 5, 1000);function createCircuitBreaker(fn, options = {}) {
const { failureThreshold = 5, resetTimeout = 60000 } = options;
let failures = 0;
let nextAttempt = Date.now();
let state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
return async function(...args) {
if (state === "OPEN") {
if (Date.now() < nextAttempt) {
throw new Error("Circuit breaker is OPEN");
}
state = "HALF_OPEN";
}
try {
const result = await fn(...args);
failures = 0;
state = "CLOSED";
return result;
} catch (error) {
failures++;
if (failures >= failureThreshold) {
state = "OPEN";
nextAttempt = Date.now() + resetTimeout;
}
throw error;
}
};
}const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(999) // This might fail
]);
const successful = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
const failed = results
.filter(r => r.status === "rejected")
.map(r => r.reason);// ❌ Promise.all fails fast (one failure fails all)
const [users, orders] = await Promise.all([
fetchUsers(),
fetchOrders()
]);
// ✅ Handle individual errors
const [usersResult, ordersResult] = await Promise.all([
fetchUsers().catch(e => ({ error: e })),
fetchOrders().catch(e => ({ error: e }))
]);Cache async results to avoid redundant calls:
function memoizeAsync(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const promise = fn.apply(this, args).catch(error => {
cache.delete(key); // Remove failed promises from cache
throw error;
});
cache.set(key, promise);
return promise;
};
}
const fetchUserMemoized = memoizeAsync(fetchUser);
// First call fetches from network
const user1 = await fetchUserMemoized(1);
// Second call returns cached promise
const user2 = await fetchUserMemoized(1);Sometimes you need to create a Promise and resolve it from outside:
function createDeferred() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
// Usage
const deferred = createDeferred();
setTimeout(() => {
deferred.resolve("Done!");
}, 1000);
const result = await deferred.promise;
console.log(result); // "Done!"Process a stream of async data:
async function* fetchPages(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// Usage
for await (const items of fetchPages("/api/items")) {
console.log(`Loaded ${items.length} items`);
}// ❌ Unhandled rejection
const promise = fetchData();
// Forgot to add .catch()!
// ✅ Always handle rejections
fetchData().catch(error => console.error(error));
// ✅ Or use try/catch with async/await
try {
const data = await fetchData();
} catch (error) {
console.error(error);
}// ❌ All requests fire simultaneously, but errors are unhandled
urls.forEach(async (url) => {
const data = await fetch(url); // Fire and forget!
});
// ✅ Sequential with for...of
for (const url of urls) {
const data = await fetch(url);
}
// ✅ Parallel with Promise.all
const results = await Promise.all(urls.map(url => fetch(url)));// ❌ Wrapping a sync value
async function getValue() {
return Promise.resolve(42);
}
// ✅ Just return the value
async function getValue() {
return 42; // async wraps it automatically
}
// ❌ await on non-promise
async function process() {
const value = await 42; // Pointless await
}
// ✅ Only await promises
async function process() {
const value = 42;
const result = await fetchData();
}// ❌ Floating promise — no error handling
async function processData() {
saveToDatabase(data); // Not awaited, errors swallowed!
}
// ✅ Properly handle
async function processData() {
await saveToDatabase(data);
}
// Or if intentionally fire-and-forget:
saveToDatabase(data).catch(error => {
console.error("Background save failed:", error);
});function myPromiseAll(promises) {
// Your implementation
}
myPromiseAll([Promise.resolve(1), Promise.resolve(2)])
.then(results => console.log(results)); // [1, 2]function myPromiseRace(promises) {
// Your implementation
}Write a function that tries a primary source, but falls back to a secondary source if the primary times out.
function fetchWithFallback(primary, fallback, timeoutMs) {
// Return primary if it completes within timeoutMs
// Otherwise return fallback
}Write a function that runs tasks in parallel and returns results for successful ones, even if some fail.
function parallelWithTolerance(tasks, maxFailures) {
// Run all tasks in parallel
// Return results if failures <= maxFailures
// Otherwise throw aggregate error
}Implement a queue that limits concurrent promise execution.
const queue = new PromiseQueue(3); // Max 3 concurrent
queue.add(() => fetchUser(1));
queue.add(() => fetchUser(2));
queue.add(() => fetchUser(3));
queue.add(() => fetchUser(4)); // Waits for a slot- Promise executors run synchronously;
.then()callbacks run as microtasks - A Promise's state is immutable once settled
- Never wrap Promises in new Promises — return them directly
- Always return Promises in
.then()to maintain the chain - Use
Promise.allfor parallel,reducefor sequential execution Promise.allSettledis safest when you need all results regardless of failures- Implement timeout, retry, and circuit breaker patterns for robustness
- Memoize async functions to avoid redundant network calls
- Always handle rejections — unhandled promise rejections crash Node.js apps
- Avoid floating promises — always await or attach
.catch()
Master async JavaScript with:
- Async/Await — the modern, clean syntax for Promises
- Error Handling — production-ready error management
- Testing Async Code — Jest and mocking strategies
Happy coding! 🚀