Understand why some work finishes later, and sequence it clearly with promises and async/await.
Why this matters
Some work takes time: loading data from a server, reading a file, waiting for a timer. If JavaScript stopped and waited for each of these, the whole page would freeze: no scrolling, no clicks, nothing. Instead, JavaScript starts the slow work, carries on, and deals with the result later. This is called asynchronous code. Every app that talks to an API depends on it, so you need to be able to read and write it confidently.
The problem in one example
console.log("1. start");
setTimeout(() => console.log("2. timer done"), 1000);
console.log("3. end");
The output is 1, 3, then 2 a second later. setTimeout does not pause the program; it schedules a function to run later and returns immediately. Any value produced inside that function is not available on the next line.
Callbacks
The oldest pattern is a callback: you pass in a function to be called when the work finishes.
function loadUser(id, callback) {
setTimeout(() => callback({ id, name: "Ada" }), 100);
}
loadUser(1, (user) => {
console.log(user.name); // "Ada", 100 ms later
});
Callbacks work, but when one step depends on another you end up nesting them inside each other ("callback hell"), and handling errors at each level gets messy.
Promises
A promise is an object that stands for a value that will arrive later. It is in one of three states: pending, fulfilled (it has a value) or rejected (it has an error). You attach what should happen next with .then and .catch:
function wait(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
wait(100, "hello")
.then((text) => text.toUpperCase())
.then((upper) => console.log(upper)) // "HELLO"
.catch((err) => console.error(err));
Each .then returns a new promise, so steps chain top to bottom instead of nesting. One .catch at the end handles a failure at any step. Most browser APIs, including fetch, return promises, so you will mostly use promises rather than create them.
async and await
async/await is a nicer way to write the same thing. Inside an async function, await pauses that function (not the whole page) until a promise settles, and gives you its value:
async function shout() {
const text = await wait(100, "hello");
return text.toUpperCase();
}
shout().then((result) => console.log(result)); // "HELLO"
Two rules to remember:
awaitonly works inside anasyncfunction (or at the top level of a module).- An
asyncfunction always returns a promise. Whatever youreturnbecomes the promise's value, so callers eitherawaitit or use.then.
Sequencing dependent steps
Suppose fetchUser(id) and fetchPosts(userId) both return promises, and you need the user before you can load their posts:
async function loadProfile(id) {
const user = await fetchUser(id); // wait for the user
const posts = await fetchPosts(user.id); // then their posts
return { name: user.name, postCount: posts.length };
}
That reads like ordinary step-by-step code, which is the whole point of await.
Running independent work in parallel
If steps do not depend on each other, awaiting them one by one wastes time. Promise.all starts them together and waits for all of them:
async function loadNames(ids) {
const users = await Promise.all(ids.map((id) => fetchUser(id)));
return users.map((u) => u.name);
}
Three requests of 100 ms each take about 100 ms in total with Promise.all, instead of 300 ms in sequence. Promise.all keeps the results in the same order as the input, and rejects as soon as any one of them fails.
Errors
A rejected promise inside await throws, so you can use ordinary try/catch:
async function safeLoad(id) {
try {
return await fetchUser(id);
} catch (err) {
console.error("Could not load user", err);
return null;
}
}
The final lesson of this module goes deeper into errors and fallbacks.
Common mistakes
- Forgetting
await.const user = fetchUser(1);gives you a pending promise, souser.nameisundefined. - Using
awaitin a normal function. It is a syntax error; mark the functionasync. - Expecting an
asyncfunction to return a plain value. It returns a promise;awaitit. - Awaiting independent work in a loop. Use
Promise.allto run it in parallel. - No error handling. An unhandled rejection fails silently in the UI; add
catchortry/catch.
Quick recap
- Asynchronous work finishes later; the rest of the code keeps running.
- Callbacks were first; promises chain with
.then/.catch. async/awaitmakes promise code read top to bottom; anasyncfunction returns a promise.awaitfor dependent steps,Promise.allfor independent ones.
Try it
Open the Do tab: use await to load a profile in order and Promise.all to load several users at once.