Promises & Async/Await
Master asynchronous JavaScript with Promises, chaining, async/await syntax, and parallel execution patterns.
Promises
A Promise is an object representing the eventual completion or failure of an asynchronous operation. Promises have three states: pending, fulfilled, and rejected. Once settled (fulfilled or rejected), a promise's state and value are immutable. The Promise constructor takes an executor function with resolve and reject callbacks, and consumers attach handlers using .then() for success and .catch() for errors. Promises solved the callback hell problem by enabling flat, chainable asynchronous code.
// Creating a Promise
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice", email: "alice@example.com" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
// Consuming a Promise
fetchUser(1)
.then((user) => {
console.log("User:", user.name);
})
.catch((err) => {
console.error("Error:", err.message);
})
.finally(() => {
console.log("Request complete");
});
// Promise.resolve and Promise.reject
const cached = Promise.resolve({ id: 1, name: "Cached User" });
cached.then((user) => console.log(user));
const failed = Promise.reject(new Error("Something broke"));
failed.catch((err) => console.log(err.message));
Promise Chaining
Promise chaining allows you to sequence asynchronous operations where each step depends on the result of the previous one. Each .then() returns a new Promise, enabling you to chain multiple operations in a flat, readable structure. If a .then() handler returns a value, the next .then() receives that value. If it returns a Promise, the chain waits for that Promise to settle before proceeding. A single .catch() at the end of a chain handles errors from any step.
function getUser(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: "Alice" }), 100);
});
}
function getPosts(userId) {
return new Promise((resolve) => {
setTimeout(
() =>
resolve([
{ id: 1, title: "Hello World", userId },
{ id: 2, title: "Async JS", userId },
]),
100
);
});
}
function getComments(postId) {
return new Promise((resolve) => {
setTimeout(
() =>
resolve([
{ id: 1, text: "Great post!", postId },
{ id: 2, text: "Thanks!", postId },
]),
100
);
});
}
// Chaining dependent async operations
getUser(1)
.then((user) => {
console.log("User:", user.name);
return getPosts(user.id);
})
.then((posts) => {
console.log("Posts:", posts.length);
return getComments(posts[0].id);
})
.then((comments) => {
console.log("Comments:", comments.length);
})
.catch((err) => {
console.error("Error in chain:", err.message);
});
Async/Await
Async/await is syntactic sugar over Promises that makes asynchronous code look and behave like synchronous code. An async function always returns a Promise, and the await keyword pauses execution until the awaited Promise settles. Error handling uses standard try/catch blocks, making it consistent with synchronous error handling patterns. Async/await makes complex asynchronous flows much easier to read, write, and debug compared to raw Promise chains.
// Async function
async function fetchUserData(userId) {
try {
const user = await getUser(userId);
console.log("User:", user.name);
const posts = await getPosts(user.id);
console.log("Posts:", posts.length);
const comments = await getComments(posts[0].id);
console.log("Comments:", comments.length);
return { user, posts, comments };
} catch (err) {
console.error("Failed:", err.message);
throw err;
}
}
// Using the async function
fetchUserData(1).then((data) => console.log("Done:", data));
// Async arrow function
const loadConfig = async () => {
const response = await fetch("/api/config");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
};
// Async in loops - sequential execution
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
async function processItem(item) {
return new Promise((resolve) =>
setTimeout(() => resolve(item.toUpperCase()), 100)
);
}
processItems(["a", "b", "c"]).then(console.log); // ["A", "B", "C"]
Promise.all and Error Handling
Promise.all takes an array of Promises and returns a single Promise that resolves when all input Promises resolve, or rejects as soon as any one rejects. This is ideal for running independent asynchronous operations in parallel. Promise.allSettled waits for all Promises to settle regardless of outcome, returning an array of result objects. Promise.race resolves or rejects with the first Promise to settle, useful for timeout patterns. Combining these with async/await creates powerful, readable concurrent code.
// Promise.all - parallel execution
async function loadDashboard(userId) {
try {
const [user, posts, notifications] = await Promise.all([
fetch(`/api/users/${userId}`).then((r) => r.json()),
fetch(`/api/posts?user=${userId}`).then((r) => r.json()),
fetch(`/api/notifications/${userId}`).then((r) => r.json()),
]);
return { user, posts, notifications };
} catch (err) {
console.error("Dashboard load failed:", err);
throw err;
}
}
// Promise.allSettled - get all results
async function fetchMultiple(urls) {
const results = await Promise.allSettled(urls.map((url) => fetch(url)));
const succeeded = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = results
.filter((r) => r.status === "rejected")
.map((r) => r.reason);
console.log(`${succeeded.length} succeeded, ${failed.length} failed`);
return succeeded;
}
// Timeout pattern with Promise.race
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]);
}
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch("https://api.example.com/data"),
5000
);
console.log("Got data:", data);
} catch (err) {
console.error(err.message); // "Timeout after 5000ms"
}
}