Promises and async/await
Order at a busy café and they hand you a buzzer. You don't stand at the counter waiting. You sit down, and the buzzer tells you when it's ready, or that they've run out.
In Timers and the Event Loop, you learned that JavaScript never stands around waiting for slow things. It hands over a callback and gets on with other work. That works fine for one step. But real tasks come in chains: load the menu, then load the prices, then show them, and if anything fails, show an error.
With callbacks alone, each step nests inside the previous one, and the code drifts further and further right until it's unreadable. Developers nicknamed that shape the "pyramid of doom." Promises were created to fix it, and async/await made them read almost like normal code.
The café buzzer
Picture ordering at a café that hands you a buzzer. That buzzer is a promise: an object that represents a result you'll get later. It's always in one of three states:
- Pending: still waiting. Your coffee is being made.
- Fulfilled: it worked, and here's the value. The buzzer lights up: coffee's ready.
- Rejected: it failed, and here's the reason. The buzzer flashes red: they're out of oat milk.
Once a promise is fulfilled or rejected, it's settled, and it never changes again.
Using a promise: then, catch, finally
Many built-in tools give you a promise. You'll meet the most important one, fetch, in the next lesson. For now, here's a small helper that makes a promise which fulfills after a delay:
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}(new Promise gives you a resolve function to call when the work succeeds, and a reject function, not used here, for when it fails. You'll mostly use promises rather than create them, so don't worry if this part feels unfamiliar.)
You react to a promise with methods:
wait(1000)
.then(() => {
console.log('One second later!');
})
.catch((error) => {
console.error('Something went wrong:', error);
})
.finally(() => {
console.log('Done either way.');
});.then(callback)runs when the promise fulfills, and receives its value..catch(callback)runs when it rejects, and receives the reason (usually an error)..finally(callback)runs either way, like thefinallyfrom Errors and Debugging.
And .then returns a new promise, so steps can be chained flat, one after another, instead of nested.
async and await: the readable way
Chains of .then are much better than nested callbacks, but there's an even cleaner way to write the same thing:
async function bakeBread() {
console.log('Mixing...');
await wait(1000);
console.log('Rising...');
await wait(1000);
console.log('Baked!');
}
bakeBread();It reads top to bottom, like the plain code you've written all along. Two new keywords make it work:
asyncin front of a function marks it as asynchronous. That lets you useawaitinside it.awaitin front of a promise means "pause this function here until the promise settles, then carry on with its value."
The key phrase is "pause this function." await doesn't freeze the page. Back in the kitchen from the last lesson, the cook sets this recipe aside, handles other tickets (clicks, timers, redrawing the screen), and comes back to it when the promise settles. You get code that looks like it waits, without anything actually blocking.
await also unwraps the value for you:
async function showMenu() {
const menu = await loadMenu(); // loadMenu returns a promise of an array
console.log(menu.length); // menu is the array itself, not a promise
}(loadMenu, and makeCoffee, loadBreads, and loadPastries further down, stand for any function that returns a promise. In the next lesson, that'll be fetch.)
Errors with try and catch
When an awaited promise rejects, await turns that into a thrown error. So you handle it with the same try and catch you already know:
async function orderCoffee() {
try {
const coffee = await makeCoffee('oat milk');
console.log('Enjoy your', coffee);
} catch (error) {
console.log('Sorry:', error.message);
}
}One tool, try and catch, now handles both ordinary errors and failed waiting. That's a big part of why async/await is so popular.
The two classic mistakes
Mistake 1: forgetting await.
async function showMenu() {
const menu = loadMenu(); // no await!
console.log(menu); // Promise {...}, not the menu
}Without await, you get the buzzer, not the coffee. When you see Promise {...} or [object Promise] where you expected real data, a missing await is almost always the reason.
Mistake 2: using await outside an async function.
function showMenu() {
const menu = await loadMenu();
// SyntaxError: await is only valid in async functions...
}Add async to the function. (Inside a module from Modules, await also works at the very top level of the file, outside any function.)
And remember: an async function always returns a promise, even if you return a plain value. Whoever calls it needs to await it (or use .then) to get the value out.
Waiting for several things at once: Promise.all
Each await waits for the previous one to finish. That's right when step two needs step one's result. But when tasks don't depend on each other, waiting one at a time wastes time:
async function loadEverything() {
// One after another: about 2 seconds in total
const breads = await loadBreads(); // 1 second
const pastries = await loadPastries(); // 1 more second
}Promise.all starts them all together and waits for every one to finish:
async function loadEverything() {
// Side by side: about 1 second in total
const [breads, pastries] = await Promise.all([loadBreads(), loadPastries()]);
}It takes an array of promises and gives back one promise of an array of results, in the same order. (That [breads, pastries] is array destructuring from Destructuring and Spread.) If any one of them rejects, the whole thing rejects, so wrap it in try and catch like any other await.
Try it
HTML
JavaScript
Preview
Console
Nothing logged yet. Anything you pass to console.log() shows up here.
Try it yourself
- Press "Bake bread" and watch the status change. Then press "Order an oat milk latte" and read the friendly error.
- Change
'oat milk'inorderCoffeeto'whole milk'so the order succeeds. - Add
awaitin front of themakeCoffee('whole milk')in the console line. What error do you get, and why? (Hint: the line isn't inside anasyncfunction.)
Check your understanding
Up next
You've been practicing with pretend coffee. Now for the real thing: asking a server for data and showing it on your page, with proper loading and error states. That's Loading Data with fetch.
