Introduction
JavaScript Promises revolutionized asynchronous programming. They replaced callback hell with clean, chainable code. Today, async/await makes promises even more elegant. This guide covers everything you need to know.
The Problem: Callback Hell
Before Promises
// ❌ Callback hell
getData((data) => {
processData(data, (result) => {
saveData(result, (saved) => {
sendNotification(saved, (sent) => {
console.log("Done!");
// Deeply nested, hard to read
});
});
});
});
Problems:
- Hard to read
- Difficult to debug
- Error handling is messy
- No easy way to cancel
What are Promises?
A Promise represents a value that may not be available yet, but will be resolved (or rejected) in the future.
Promise States
Pending → Fulfilled (Resolved)
↓
Rejected
Three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Creating Promises
// Explicit Promise creation
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Data loaded");
} else {
reject("Error occurred");
}
}, 1000);
});
Using Promises
Basic Promise Usage
const promise = fetch("/api/data");
promise
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error));
Promise Chaining
fetch("/api/users")
.then((response) => response.json())
.then((users) => users[0])
.then((user) => fetch(`/api/users/${user.id}/posts`))
.then((response) => response.json())
.then((posts) => console.log(posts))
.catch((error) => console.error("Error:", error));
Promise Methods
Promise.all()
Wait for all promises to resolve:
const promise1 = fetch("/api/data1");
const promise2 = fetch("/api/data2");
const promise3 = fetch("/api/data3");
Promise.all([promise1, promise2, promise3])
.then((responses) => {
// All resolved
return Promise.all(responses.map((r) => r.json()));
})
.then((results) => {
console.log(results); // [data1, data2, data3]
})
.catch((error) => {
// If ANY promise rejects, this catches it
console.error(error);
});
Promise.allSettled()
Wait for all promises to settle (resolve or reject):
const promises = [
fetch("/api/data1"),
fetch("/api/data2"), // Might fail
fetch("/api/data3"),
];
Promise.allSettled(promises).then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Promise ${index}:`, result.value);
} else {
console.error(`Promise ${index}:`, result.reason);
}
});
});
Promise.race()
First promise to settle wins:
const fastAPI = fetch("/api/fast");
const slowAPI = fetch("/api/slow");
Promise.race([fastAPI, slowAPI])
.then((response) => {
// First response wins
console.log("Got response:", response);
})
.catch((error) => {
// First error wins
console.error("Error:", error);
});
Promise.any()
First promise to fulfill (ignores rejections):
const backup1 = fetch("/api/primary");
const backup2 = fetch("/api/backup1");
const backup3 = fetch("/api/backup2");
Promise.any([backup1, backup2, backup3])
.then((response) => {
// First successful response
console.log("Got data from:", response.url);
})
.catch((error) => {
// All promises rejected
console.error("All APIs failed");
});
Async/Await
Basic Syntax
// Before: Promises
fetch("/api/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error));
// After: async/await
async function fetchData() {
try {
const response = await fetch("/api/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchData();
Multiple Awaits
async function getUserData(userId) {
const user = await fetch(`/api/users/${userId}`);
const posts = await fetch(`/api/users/${userId}/posts`);
const comments = await fetch(`/api/users/${userId}/comments`);
return {
user: await user.json(),
posts: await posts.json(),
comments: await comments.json(),
};
}
Parallel Execution
// Sequential (slow)
async function sequential() {
const data1 = await fetch("/api/data1");
const data2 = await fetch("/api/data2");
const data3 = await fetch("/api/data3");
// Total time: time1 + time2 + time3
}
// Parallel (fast)
async function parallel() {
const [data1, data2, data3] = await Promise.all([
fetch("/api/data1"),
fetch("/api/data2"),
fetch("/api/data3"),
]);
// Total time: max(time1, time2, time3)
}
Error Handling
Try/Catch with Async/Await
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch failed:", error);
// Handle error or rethrow
throw error;
}
}
Multiple Error Handling
async function complexOperation() {
try {
const user = await getUser();
try {
const posts = await getPosts(user.id);
return { user, posts };
} catch (postError) {
// Handle post error specifically
return { user, posts: [] };
}
} catch (userError) {
// Handle user error
throw new Error("Failed to load user");
}
}
Common Patterns
Retry Logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response;
throw new Error("Request failed");
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Timeout
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout)
),
]);
}
Sequential Processing
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
Best Practices
✅ Do This
1. Always handle errors
async function safeOperation() {
try {
return await riskyOperation();
} catch (error) {
console.error("Operation failed:", error);
return defaultValue;
}
}
2. Use Promise.all for parallel operations
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
3. Return promises from async functions
async function getData() {
const response = await fetch("/api/data");
return response.json(); // Returns a Promise
}
❌ Don’t Do This
1. Don’t forget await
// ❌ Missing await
async function fetchData() {
const data = fetch("/api/data"); // Returns Promise, not data!
return data;
}
// ✅ Correct
async function fetchData() {
const response = await fetch("/api/data");
return response.json();
}
2. Don’t mix promise chains with async/await unnecessarily
// ❌ Unnecessary mixing
async function fetchData() {
return fetch("/api/data")
.then((response) => response.json())
.then((data) => data.items);
}
// ✅ Cleaner with async/await
async function fetchData() {
const response = await fetch("/api/data");
const data = await response.json();
return data.items;
}
3. Don’t create promise chains in loops
// ❌ Creates promises sequentially
for (const url of urls) {
await fetch(url); // Waits for each
}
// ✅ Parallel execution
await Promise.all(urls.map((url) => fetch(url)));
Visualizing Promises
Use our tools:
- Promise Visualizer - See promise chains visually
Conclusion
Promises and async/await make JavaScript asynchronous code:
Key benefits:
- Clean, readable code
- Better error handling
- Easy to chain operations
- Parallel execution support
Remember:
- Always handle errors
- Use async/await for readability
- Use Promise.all for parallel operations
- Don’t forget await!
Next Steps
- Try Promise Visualizer
- Learn about Event Loop
- Explore Error Handling Patterns