JavaScript Promises & Async/Await
Master asynchronous JavaScript with Promises and async/await. Learn error handling, Promise.all, chaining, and real-world patterns for API calls and data fetching.
JavaScript Promises & Async/Await
Asynchronous code is fundamental to JavaScript. Whether you're fetching data from an API, reading a file, or waiting for user input — you need to understand how Promises and async/await work.
The Problem: Callback Hell
Before Promises, we used callbacks for async operations:
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
});
});
});
This nesting — called "callback hell" or the "pyramid of doom" — is hard to read, hard to debug, and hard to handle errors in.
Promises: The Solution
A Promise represents a value that will be available in the future. It has three states:
- Pending — still waiting
- Fulfilled — completed successfully
- Rejected — failed with an error
Creating a Promise
const promise = new Promise((resolve, reject) => {
// Do something async
const success = true;
if (success) {
resolve("It worked!");
} else {
reject(new Error("Something went wrong"));
}
});
Using a Promise
promise
.then(result => console.log(result)) // "It worked!"
.catch(error => console.error(error));
Chaining Promises
Each .then() returns a new Promise, so you can chain them:
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("Something failed:", error));
The catch at the end handles errors from ANY step in the chain. This is much cleaner than nested callbacks.
Async/Await: Promises Made Pretty
async/await is syntactic sugar over Promises. It make async code look synchronous:
async function getUserPosts() {
const userResponse = await fetch("/api/user");
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
return posts;
}
Rules
awaitcan only be used inside anasyncfunction- An
asyncfunction always returns a Promise awaitpauses execution until the Promise resolves
Error Handling with try/catch
async function getUserPosts() {
try {
const response = await fetch("/api/user");
if (!response.ok) throw new Error("Failed to fetch user");
const user = await response.json();
return user;
} catch (error) {
console.error("Error:", error.message);
return null;
}
}
Promise Utility Methods
Promise.all — Run in Parallel
Wait for ALL promises to complete. Fails fast if any one rejects.
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()),
]);
Use this when requests are independent and you want them to run simultaneously.
Promise.allSettled — No Fail-Fast
Wait for all promises, regardless of whether they succeed or fail:
const results = await Promise.allSettled([
fetch("/api/critical"),
fetch("/api/optional"),
]);
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
Promise.race — First One Wins
Returns the result of whichever promise settles first:
const result = await Promise.race([
fetch("/api/fast-server"),
fetch("/api/slow-server"),
]);
Useful for implementing timeouts:
function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
),
]);
}
Promise.any — First Success Wins
Like race, but ignores rejections. Only rejects if ALL promises reject:
const result = await Promise.any([
fetch("https://cdn1.example.com/data"),
fetch("https://cdn2.example.com/data"),
fetch("https://cdn3.example.com/data"),
]);
Common Patterns
Sequential Execution
When order matters:
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
Don't use forEach with await — it doesn't wait:
// BROKEN — these all fire at once!
items.forEach(async (item) => {
await processItem(item);
});
Parallel Execution with Limit
Process items in batches:
async function processInBatches(items, batchSize) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
Retry Pattern
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
Microtasks vs Macrotasks
Promise callbacks (.then) are microtasks — they execute before the next macrotask (like setTimeout):
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
Understanding this order helps debug timing issues.
Key Takeaways
- Promises represent future values — use
.then()/.catch()orasync/await async/awaitmakes async code readable — prefer it over raw.then()chains- Use
Promise.allfor parallel independent operations - Always handle errors with
try/catchor.catch() - Never use
forEachwithawait— usefor...ofinstead - Promise.allSettled is great when you want results regardless of individual failures
🚀 Practice What You Learned
Apply these concepts with hands-on coding challenges: