JavaScript · Lesson 5 of 6
Promises and async/await
Handle work that finishes later: timers, fetch and error handling.
- Intermediate
- 16 min read
- 3 objectives
Before this lessonLesson 4: Arrays and Objects
What you will learn
- Explain the event loop
- Use async/await
- Run requests in parallel
JavaScript runs your code on a single thread. To stay responsive while waiting for things like network requests or timers, it hands slow work to the environment and continues. When the result is ready, a callback is queued and runs once the current code has finished. This mechanism is the event loop.
Why waiting is hard
Some work is slow: loading data from a server, reading a file, waiting for a timer. JavaScript has a single main thread, so if it simply stood still until each slow task finished, the whole page would freeze. Instead it starts the slow job, carries on with other work, and comes back when the result is ready. Think of ordering coffee: you give your order, take a buzzer, sit down, and collect the drink when the buzzer goes off. The buzzer is a Promise.
console.log("1: order coffee");
setTimeout(() => console.log("3: coffee is ready"), 100);
console.log("2: check phone while waiting");1: order coffee 2: check phone while waiting 3: coffee is ready
Notice the order. The timer's message prints last, even though it was written second. The code did not wait; it scheduled the callback and moved on.
console.log("1 start");
setTimeout(() => console.log("3 timer"), 0);
console.log("2 end");1 start 2 end 3 timer
Even with a delay of 0, the timer callback runs after the synchronous code finishes.
Promises
A promise stands for a value that will arrive later. It is pending, then either fulfilled with a value or rejected with an error. Chain .then() for success and .catch() for failure.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(100)
.then(() => console.log("done waiting"))
.catch((err) => console.error(err));async / await
async functions always return a promise, and await pauses the function until a promise settles, so asynchronous code reads top to bottom. Wrap awaits in try/catch to handle errors.
async function loadUser(id) {
try {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("failed:", err.message);
return null;
}
}
loadUser(1).then(console.log);Parallel work
Awaiting things one after another makes them run in sequence. When the calls are independent, start them together with Promise.all, which resolves when all are done and rejects if any fails.
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
async function main() {
console.time("parallel");
const [a, b] = await Promise.all([wait(200, "A"), wait(200, "B")]);
console.timeEnd("parallel"); // ~200ms, not 400ms
console.log(a, b);
}
main();parallel: 201ms A B
Promise.allSettled waits for every promise and reports each outcome, which is handy when partial failure is acceptable.
A promise has three states
- pending: the work has started and is not finished.
- fulfilled: it finished and produced a value.
- rejected: it failed and produced an error.
const wait = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
wait(50, "done").then(v => console.log("resolved with", v));
console.log("this prints first");this prints first resolved with done
Same thing, easier to read: async and await
await pauses that function until the promise settles, then gives you the value as if the code were ordinary top-to-bottom. It can only be used inside an async function. Other code keeps running while you wait.
const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));
async function main() {
console.log("start");
const a = await wait(50, "first");
const b = await wait(50, "second");
console.log(a, b);
console.log("end");
}
main();start first second end
Handling failure
const failing = () => new Promise((_, reject) => setTimeout(() => reject(new Error("server down")), 20));
async function load() {
try {
await failing();
} catch (err) {
console.log("caught:", err.message);
} finally {
console.log("cleanup");
}
}
load();caught: server down cleanup
Sequential versus parallel
Two awaits in a row run one after the other, which is slow if the tasks are independent. Start them together and wait for both with Promise.all.
const wait = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));
async function main() {
let t = Date.now();
await wait(100); await wait(100);
console.log("one by one:", Math.round((Date.now() - t) / 100) * 100, "ms");
t = Date.now();
const [a, b] = await Promise.all([wait(100, "x"), wait(100, "y")]);
console.log("together:", Math.round((Date.now() - t) / 100) * 100, "ms", a, b);
}
main();one by one: 200 ms together: 100 ms x y
A realistic fetch example
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json(); // also returns a promise
}
getUser(1)
.then(user => console.log(user.name))
.catch(err => console.error(err.message));Common mistakes
- Forgetting
await, so you log a pending promise instead of its value. - Using
awaitinsideforEach; it does not wait. Use afor...ofloop orPromise.allwithmap. - Not handling rejections, which produce "unhandled promise rejection" errors.
Key takeaways
- Slow work returns a Promise (pending, fulfilled or rejected) so the page never freezes.
async/awaitlets you read asynchronous code top to bottom.- Wrap awaits in
try/catch; checkresponse.okwhen usingfetch. - Use
Promise.allfor independent tasks that can run together.
// Write your solution here
