Free Online Code Editor
Understand JavaScript promises and async/await step by step, including running tasks in parallel with Promise.all and the mistakes that cause silent bugs.
Some things take time: loading data from a server, reading a file, waiting for a timer. JavaScript doesn't stop and wait for them. It keeps running your other code and comes back when the slow thing is done.
That's useful, but it changes how you write code. This guide walks through the three ways to handle it, and why you'll almost always want the last one.
Look at this code. What do you expect it to print?
console.log("1: start");
setTimeout(() => {
console.log("2: timer finished");
}, 100);
console.log("3: end");1: start
3: end
2: timer finishedThe timer callback runs last, even though it appears in the middle. JavaScript schedules it and moves on. Everything about async code comes from this one fact.
The first solution was to pass a function that gets called when the work finishes.
function getUser(id, callback) {
setTimeout(() => callback({ id, name: "Ana" }), 50);
}
function getOrders(userId, callback) {
setTimeout(() => callback(["book", "pen"]), 50);
}
getUser(1, (user) => {
getOrders(user.id, (orders) => {
console.log(user.name, "ordered", orders.join(" and "));
});
});Ana ordered book and penIt works, but each new step nests deeper. Add error handling at every level and you get the famous "callback pyramid." It's hard to read and harder to debug.
A promise is an object that stands in for a result you don't have yet. It's always in one of three states: pending, fulfilled (it worked), or rejected (it failed).
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getUser(id) {
return wait(50).then(() => ({ id, name: "Ana" }));
}
function getOrders(userId) {
return wait(50).then(() => ["book", "pen"]);
}
getUser(1)
.then((user) => getOrders(user.id).then((orders) => ({ user, orders })))
.then(({ user, orders }) => {
console.log(user.name, "ordered", orders.join(" and "));
})
.catch((error) => console.error("Something failed:", error));Ana ordered book and penThe chain is flat, and a single .catch() handles an error from any step. That's already a big improvement.
async and await are a cleaner way to use promises. Inside an async function, await pauses that function until the promise settles, then gives you the value.
async function showOrders() {
const user = await getUser(1);
const orders = await getOrders(user.id);
console.log(user.name, "ordered", orders.join(" and "));
}
showOrders();Ana ordered book and penSame result, but it reads top to bottom. Errors work with the try/catch you already know:
async function risky() {
try {
await Promise.reject(new Error("server down"));
} catch (error) {
console.log("Caught:", error.message);
}
}
risky();Caught: server downTwo facts to remember: an async function always returns a promise, and await only works inside an async function (or at the top level of a module).
Here's a mistake that makes apps slower than they need to be. These two requests don't depend on each other, but the code runs them one after the other:
async function slow() {
const start = Date.now();
await wait(300);
await wait(300);
console.log("sequential:", Math.round((Date.now() - start) / 100) * 100, "ms");
}
async function fast() {
const start = Date.now();
await Promise.all([wait(300), wait(300)]);
console.log("parallel:", Math.round((Date.now() - start) / 100) * 100, "ms");
}
slow().then(fast);sequential: 600 ms
parallel: 300 msIf the tasks are independent, start them all first and wait for them together. The total time is the slowest one, not the sum.
Promise.all waits for everything and fails fast. If any promise rejects, the whole thing rejects.Promise.allSettled waits for everything and never rejects. You get a report on each one.Promise.race settles as soon as the first promise does. Useful for timeouts.Promise.any resolves with the first one that succeeds.async function demo() {
const results = await Promise.allSettled([
Promise.resolve("ok"),
Promise.reject(new Error("nope")),
]);
results.forEach((r) => console.log(r.status));
}
demo();fulfilled
rejectedUse allSettled when partial success is acceptable, such as loading several widgets on a dashboard where one failing shouldn't blank the page.
Forgetting await. You get a promise object instead of the value, and no error.
const user = getUser(1); // promise, not a user
console.log(user.name); // undefinedUsing await inside forEach. forEach doesn't wait for async callbacks. Use a for...of loop for sequential work, or Promise.all with map for parallel work.
async function loop() {
const ids = [1, 2, 3];
for (const id of ids) {
const user = await getUser(id);
console.log("loaded", user.id);
}
}
loop();loaded 1
loaded 2
loaded 3Unhandled rejections. If a promise fails and nothing catches it, you'll see an error in the console or the failure gets lost. Always have a catch somewhere.
Not checking response.ok with fetch. fetch only rejects on network failure. A 404 or 500 still resolves, so check if (!response.ok) yourself.
async/await. It's the same promises with far better readability.Promise.all.await.Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.
Open the JavaScript editorLearn how to find elements, change text and classes, create new nodes, and handle clicks with event delegation, plus how to avoid common XSS mistakes.
A hands-on tour of the modern JavaScript features that make everyday code shorter and safer, from destructuring and ?. to structuredClone and toSorted.
Learn how to catch, throw, and design errors in JavaScript, including custom error classes, finally, and the async mistakes that let failures slip through.