Async JavaScript: Promises and async/await
Learn JavaScript promises, async/await, and Promise.all with practical examples for faster, reliable frontend data fetching.
Most frontend work is waiting: waiting for a user profile, a product list, or an image to finish loading. JavaScript handles this waiting with promises and async/await.
This guide shows the small set of patterns that covers nearly every professional use case: chaining dependent requests, running independent requests in parallel, and turning event-based APIs into promises.
What are JavaScript promises?
A promise represents a value that is not ready yet. It starts as pending, then becomes fulfilled with data or rejected with an error.
You consume it in two equivalent ways. The .then() style chains short pipelines. The async/await style reads like normal sequential code and keeps error handling in one place.
Why async patterns matter at work
Slow pages are often not a server problem. They are a frontend sequencing problem: three independent requests awaited one after another instead of together.
- Dashboards and detail pages commonly need 3 to 5 independent resources on load.
- Unhandled rejections break user flows and flood error monitoring with noise.
- Image, file, and geolocation APIs still use events, so wrapping them in promises keeps code consistent.
How to wrap an event API in a promise
Image loading uses onload and onerror events rather than promises. Wrapping it gives you a retry with a fallback image and a clean error path.
function loadImage(src, fallbackSrc) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => {
if (fallbackSrc && img.src !== fallbackSrc) {
img.src = fallbackSrc;
} else {
reject(new Error("Image failed to load: " + src));
}
};
img.src = src;
});
}
const img = await loadImage("/avatars/me.png", "/avatars/default.png");
document.getElementById("avatar").replaceWith(img);The surrounding page code stays linear. Either the avatar swaps in, or the error handler shows a placeholder and reports the failure.
How to chain dependent requests
Some requests truly depend on each other. Fetching a user's post titles needs the user ID first, then the posts for that user.
async function fetchJson(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error("Request failed: " + res.status);
return res.json();
}
async function getPostTitles(userId) {
const user = await fetchJson("/api/users/" + userId);
const posts = await fetchJson("/api/posts?userId=" + user.id);
return posts.map((p) => p.title);
}When to prefer async/await over .then()
Use async/await when there is branching, shared state, or one try/catch for the whole flow. Use .then() for short linear transforms with a single mapping step.
How to run independent requests in parallel
When requests do not depend on each other, start them together. Three 200ms requests take about 200ms in parallel instead of 600ms in sequence.
const [user, settings, feed] = await Promise.all([
fetchJson("/api/user"),
fetchJson("/api/settings"),
fetchJson("/api/feed"),
]);
const results = await Promise.allSettled([
fetchJson("/api/user"),
fetchJson("/api/recommendations"),
]);
const loaded = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = results.filter((r) => r.status === "rejected");Promise.all vs Promise.allSettled
Promise.all is strict: one rejection rejects everything. Use it when every result is required, such as a checkout page.
Promise.allSettled is tolerant: it returns the status of every request. Use it for dashboards and feeds where one failed widget should not blank the whole screen.
How to handle large lists without overload
Fetching hundreds of items with one giant Promise.all can flood the API. Process IDs in small batches instead.
async function fetchUsers(userIds, limit = 5) {
const out = [];
for (let i = 0; i < userIds.length; i += limit) {
const batch = userIds.slice(i, i + limit);
const users = await Promise.all(batch.map((id) => fetchJson("/api/users/" + id)));
out.push(...users);
}
return out;
}Real-world use cases
- Product pages: load product, reviews, and recommendations together.
- Admin dashboards: render working widgets even when one API fails.
- Media uploads: wrap file reads and image loads in promises with fallbacks.
Best practices for professional code
- Centralize status checks in one fetchJson helper instead of repeating res.ok logic.
- Return or await every inner promise so chains never silently stop.
- Add AbortController timeouts so a hung request cannot freeze the UI.
- Keep promise output order in mind: Promise.all preserves input order.
Common mistakes to avoid
- Awaiting independent requests one by one inside the same function.
- Using await inside a loop for hundreds of items without batching.
- Catching errors with an empty block instead of showing a fallback state.
Frequently asked questions
Does Promise.all run requests in order?
It starts all requests together and returns results in the same order as the input array, regardless of which request finishes first.
Should everything use async/await?
No. Short data transforms read well with .then(). Longer flows with branching and shared error handling read better with async/await.
Key takeaways
- Chain only truly dependent requests; parallelize the rest.
- Use Promise.all for all-or-nothing flows and allSettled for dashboards.
- Wrap event-based APIs so every async unit returns a promise.