Waiting Without Freezing
Getting data from a server takes time: a fraction of a second on a fast connection, several seconds on a phone in a train. A page that stopped and waited would freeze, with no clicks, no typing, nothing. JavaScript's answer is to ask, carry on with everything else, and deal with the answer when it arrives. The tool for that is the promise, and two keywords, async and await, make code that uses promises read like any other code.
Think of a café that hands you a buzzer when you order. You do not stand frozen at the counter until your coffee is made. You sit down, keep talking, and collect it when the buzzer goes off. Or the buzzer goes off and the barista tells you they are out of milk. Both endings are an answer, and neither kept you standing at the counter.
Why Can't a Page Just Wait?
A page's JavaScript runs one piece of code at a time, and it shares that single line of work with the browser's own jobs on the page: answering clicks, handling key presses, updating what you see. While a piece of your code is running, none of those can happen. Most code finishes in a thousandth of a second, so nobody notices. Code that takes longer is noticed at once.
<button type="button" id="freeze">Freeze for 3 seconds</button> <button type="button" id="count">Clicked 0 times</button>
const count = document.querySelector("#count"); let clicks = 0; count.addEventListener("click", () => { clicks = clicks + 1; count.textContent = `Clicked ${clicks} times`; }); document.querySelector("#freeze").addEventListener("click", () => { const end = Date.now() + 3000; while (Date.now() < end) { // busy doing nothing, on purpose } });
Put the two buttons in a practice page and the script in a deferred script file, as in Chapter 6. The second button counts its clicks and shows the number on itself. The first runs a loop for three seconds: Date.now() gives the current time in milliseconds, and the while loop keeps checking it until three seconds have passed. Click the counter a few times: it updates at once. Now click Freeze and then the counter, several times. For three seconds nothing on the page changes. Then the count usually jumps, as the browser finally handles the clicks it was holding.
A request over the network can take far longer than three seconds. If waiting for one worked like that loop, every page would lock up every time it loaded anything. So the browser does the waiting itself, outside your code, and hands your code a promise instead.
A Promise: A Value That Arrives Later
A promise is an object that stands for a value that is not here yet. It starts pending. Later it becomes fulfilled, meaning the value arrived, or rejected, meaning it failed, with an error that says why. Once it has settled either way, it never changes again. In the café, the buzzer in your hand is the promise, the coffee is the value, and the missing milk is the rejection. From here on, the real words.
You will rarely make promises yourself: fetch and other browser features hand them to you. To watch one without a network, the example below makes its own. setTimeout asks the browser to run a function after a number of milliseconds, and the browser does that waiting without freezing anything. The wait function wraps it in a promise that is fulfilled after the delay. The line starting with new Promise is the only unusual part, and you can use it without writing another like it.
function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function order() { console.log("Order placed"); await wait(2000); console.log("Coffee is ready"); } order(); console.log("Still talking");
Order placed Still talking Coffee is ready
Paste it into the Console. Order placed appears first. Still talking appears immediately after it, even though its line comes last in the code. Coffee is ready appears two seconds later. A grey undefined may show up before it: that is the Console echoing the value of the last line, as it always does, and it can be ignored. The order function paused at the await, and while it waited the rest of the code carried on.
async and await
Write await in front of a promise and the function it is in pauses there until the promise settles, then carries on with the value. Only that function pauses. The rest of the page, other code, clicks, typing, keeps running. await is allowed only inside a function marked async, and an async function always returns a promise itself, so whoever calls it can wait for it in turn.
The site's js/main.js uses one shape for all its waiting code: a function called main, marked async, called once on the last line of the file. JavaScript modules can also use await outside any function, but that is not yet marked Baseline widely available, so this book does not rely on it.
async function main() { // waiting code goes here, with await } main();
You will also meet older code that writes .then() after a promise and passes it a function to run with the value. It is the same mechanism written a different way. await lets the code read top to bottom, in the order things happen.
When the Wait Fails
When an awaited promise is rejected, the await line throws an error, just as if that line itself had gone wrong. try and catch, met in Chapter 9, handle it: the code in the try block runs, and if anything in it throws, the catch block runs instead, with the error.
function failLater(ms) { return new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Out of milk")), ms); }); } async function orderLatte() { try { await failLater(1000); console.log("Coffee is ready"); } catch (error) { console.log("Sorry:", error.message); } } orderLatte();
This promise is rejected after one second, with an error whose message is Out of milk. The await throws, Coffee is ready is skipped, and the catch block logs Sorry: Out of milk. The function handled its own failure and finished normally.
Now the mistake: remove the try and catch and run it again. The Console shows a red error that begins with the words Uncaught (in promise), meaning a promise was rejected and nobody caught it, followed by the error and its message, Out of milk. The visitor sees nothing at all. The page simply stays as it was, and a page that silently does nothing looks broken. So catch failures where you can show the visitor something, as the rest of this chapter does.
- "await stops the whole page." It pauses only the function it is in. Clicks, typing and other code carry on.
- "A promise is the data." It is a promise of data.
awaitit to get the value itself. - "Errors in async code show up where I expect." A rejection nobody catches appears only in the Console. Catch it where you can tell the visitor what happened.
- Every request in the rest of this book uses this pattern:
async,await,tryandcatch. - The page stays responsive while the network is slow, which on a phone is much of the time.
Knowledge Check
While an await is waiting for a promise, what is paused?
- Every script on the page, until it settles
- The whole page, including clicks and typing
- Only the async function that contains it
- Nothing, since the next line runs at once
Which list names the states a promise can be in?
- Started, then running, then finished
- Pending, then fulfilled or rejected
- Waiting, then loaded or left empty
- Sent, then received, then read
An awaited request fails and nothing catches the error. What does the visitor see?
- A message box from the browser with the error
- A blank page, because the error stopped everything
- The page reloads and tries the request again
- No change at all; only the Console shows it
order logs A, awaits a two-second wait, then logs C. The code calls order() and then logs B. In what order do the three appear?
- A, then B, then C two seconds later
- A, then C two seconds later, then B
- B, then A, then C two seconds later
- Nothing for two seconds, then A, B, C
You got correct