The starting point
The page is already built: two cards, a clock and a text field. Together, we only write the calls. Each of you gets your own invented student, with invented courses and grades: nothing to set up.
- The clock is JavaScript on purpose: a CSS animation could keep turning when JavaScript cannot. Watch it during every load: after each one, the line beside the button says how long it took, and whether the page froze.
- The text field: type in it while the dashboard loads.
- Ready-made, in the HTML tab (index.html):
showAnnouncements(list),showStudent(me),showCourse(course),showGrade(grade),showError(card, message)(card:'announcements'or'grade'), andstart(), which empties the cards and starts the stopwatch. - Your part, in the JS tab (script.js):
loadAnnouncements(),loadGrade()and the Load button.API, on line 2, is your own address. - A typo or a mistake? The line next to the button turns red. An error your
catchhandles shows in its card; one nobody catches says “Uncaught error”, and the playground’s Console says where: an “HTML line” is the ready-made code; your mistake is at the first plain “line”, in the JS tab.
How the playground behaves (read when stuck)
- “Loading…” stays until both cards are filled or show an error: a card your code does not write yet keeps it there, and 2 s after the last answer the line says so. Run, at the top, restarts the page; the calls start only when you press Load the dashboard. Auto-run restarts the page at every change in your code: press Load again after you type. To type a whole function, turn Auto-run off, then press Run (Ctrl+Enter).
- A solution opens in a new tab, for reading: the code you saved stays as it is. To go on from the solution, press “Keep this code here” in that tab: it replaces your saved code. Then close your old tab (if it says “Changed in another tab”, press “Load that version” there).
- The editor closes brackets and quotes for you: when you reach a
)or}it already wrote, press End or →, do not type it again. Click in the code first: Shift+Alt+F puts the indentation right, Ctrl+Z undoes. - Your code shows as text in the preview, or the Console says “Cannot set properties of null (setting 'onclick')”? Your code went into the HTML tab: press Repair the page.
// ---------- Your code ----------
function loadAnnouncements() {
// 1 call: GET API + '/announcements' -> showAnnouncements(list)
}
function loadGrade() {
// 3 calls, each one needs the answer of the one before:
// 1. GET API + '/me'
// -> showStudent(me)
// 2. GET API + '/students/' + me.id + '/courses'
// -> showCourse(courses[0])
// 3. GET API + '/students/' + me.id + '/courses/' + courses[0].id + '/grade'
// -> showGrade(grade)
// A call fails? showError('grade', message)
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
The four addresses and their answers: the API documentation, below. Working beside the playground? Give it at least 900 px (narrower, it shows one panel at a time) and open the monitor alone in the rest.
On a phone, the Web Playground works (Code, Preview and Console at the bottom), and the monitor stays in this tab: come back to it after each Load. On an iPhone or iPad, and in Safari on a Mac, the preview hides error messages (the Console only says “Script error.”): a computer with Chrome, Edge or Firefox makes this lab easier.
Live monitor
What the server sees
Each call your dashboard makes appears here as a bar, from the moment it arrives at the server to the moment it is answered. In line or side by side: you see it without opening the developer tools. The four latest runs stay on screen, numbered, the newest on top; the pencil names one (“sync”, “trap”). Dashed lines mark 6 s and 8 s, the two times this lab is about.
No call yet. Press Load in your dashboard. A card says HTTP 401? Open the starting point again.
Open the monitor alone, for the projector ↗
Someone else is using your session? Open this page with ?new=1: you get a fresh one (then reopen the starting point, and the monitor alone if it is open).
API documentation
The lab server
In your code, every address starts with API, the constant on line 2: your own address on the lab server. A plain address in your code is not your student: it lands in a session shared by your whole network, and your monitor never sees it. Opened in a tab of this browser, the plain addresses answer for the same student: https://api.mehditmimi.com/me.
| Call | Answer (JSON) |
|---|---|
GET /announcements | [{ date, title, text }, …] three announcements |
GET /me | { id, name, program } your student |
GET /students/ | [{ id, title }, …] your three courses; {id} comes from /me |
GET /students/ | { studentId, courseId, course, grade, outOf } a grade out of 20; {courseId} comes from /courses |
- The server waits 2 seconds before it answers each call to one of these four addresses; any other address answers
404at once. - Options, at the end of the address:
?delay=1waits 1 second instead (0 to 5);?fail=1answers500after the wait. Both:?delay=1&fail=1. - Errors, with a JSON body
{ "error": "…" }:404an address that does not exist, or an id that is not yours,401anAPIaddress the server does not know,429too many calls (wait a little),500withfail=1.
{
"id": 8174,
"name": "Ilyas Fassi",
"program": "Bachelor, software engineering"
}
Synchronous XHR
My code in Web Playground ↗Page broken? Repair it (keeps my code) ↗Write getSync(url): const xhr = new XMLHttpRequest();, then xhr.open('GET', url, false): false means “wait right here”. xhr.send() then returns only when the server has answered, and getSync returns that answer as an object. Use it for the four calls, inside a try/catch whose catch calls showError.
Press Load once, then type in the text field. What does the clock do? How long until the cards fill, and do they fill one by one?
xhr.responseTextis the server’s answer as text;JSON.parseturns it into an object.- The
/coursescall needsme.id, the/gradecall needscourses[0].idtoo: each line waits for the one before, so that is easy here. - Check
xhr.statusbefore you parse: if it is not200,throw new Error('HTTP ' + xhr.status); yourcatchshows it in the card.
Check after you predict
The clock stops for about 8 seconds: the dashboard page is frozen, while your lab tab keeps drawing the monitor. What you type in the text field appears only when the page is free again, all at once. Then both cards fill at the same moment: while it waited, the page could not even repaint, so “Loading…” never showed. The line beside the button says it too: “page frozen”.
In the monitor: four bars, one after the other. On a computer, Chrome’s own developer tools (F12, the Issues tab; not the playground’s Console) even flag synchronous XMLHttpRequest on the main thread as deprecated: never do this on a real page.
Clicked Load again during the freeze?
The second click waited too, like your keystrokes: it runs once the first load ends, and the page freezes another 8 s. The line still says about 8 s, and “page frozen” about 8 s too: the second click restarted the stopwatch and the freeze meter. The monitor shows the truth: 8 calls in line, the second load right after the first (two runs of about 8 s back to back, or one run of about 17 s). The disabled button did not protect you: it was disabled and enabled again before the page could even repaint.
Change '/courses' into '/courses?fail=1' (inside the quotes): the server answers 500 after its wait. What does the card say? Then check xhr.status in getSync (hint 3) and load again.
What happened
Without the check, the server’s error is valid JSON, so courses is the error object, courses[0] is undefined, and showCourse(courses[0]) throws a TypeError: your catch shows “Cannot read properties of undefined (reading 'title')” in the card, an error that says nothing about the server (with no try/catch, the line says “Uncaught error” and the card waits). With the check, throw stops the function and the card shows HTTP 500.
Solution
// ---------- Your code: synchronous XHR ----------
function getSync(url) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // false: wait here for the answer
xhr.send(); // returns only when the server has answered
if (xhr.status !== 200) throw new Error('HTTP ' + xhr.status);
return JSON.parse(xhr.responseText);
}
function loadAnnouncements() {
try {
showAnnouncements(getSync(API + '/announcements'));
} catch (err) {
showError('announcements', err.message);
}
}
function loadGrade() {
try {
const me = getSync(API + '/me');
showStudent(me);
const courses = getSync(API + '/students/' + me.id + '/courses');
showCourse(courses[0]);
const grade = getSync(API + '/students/' + me.id + '/courses/' +
courses[0].id + '/grade');
showGrade(grade);
} catch (err) {
showError('grade', err.message);
}
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
Callbacks
My code in Web Playground ↗Page broken? Repair it (keeps my code) ↗Now xhr.open('GET', url, true): true means “do not wait, call me back”. xhr.send() returns at once; xhr.onload runs later, when the answer arrives. Write get(url, callback): it calls callback(null, data) when it works, callback(error) when it fails (error first: Node’s convention, you will meet it again in part 2). Then rewrite the inside of loadAnnouncements() and loadGrade() with it; delete getSync, keep the Load button.
Will the dashboard still take 8 seconds? Where must the call for the courses start?
- Code written after
get(...)runs before the answer arrives: the answer exists only inside the callback. - So the
/coursescall goes inside the callback of/me, and the/gradecall inside the callback of/courses. Write the empty shape first,get(url, function (err, me) { });, then fill it. - Every callback of
loadGrade()starts withif (err) return showError('grade', err.message);(inloadAnnouncements(), the card is'announcements'). xhr.onloadruns for every answer, a500too: checkxhr.statusthere.xhr.onerrorruns when no answer comes at all (network down).
Check after you predict
The clock keeps running and you can type. The announcements arrive after 2 s, the grade after 6 s: the two cards wait at the same time, so the dashboard takes 6 s, not 8. Your code still does one thing at a time: the browser waits for both answers, and each callback runs when its answer has arrived and the page’s one thread is free.
In the monitor: announcements beside me, then courses, then grade. In the code: three callbacks inside each other, and an error check at each level. That shape has a name: callback hell.
Add three lines: console.log('B') first in the callback of /announcements, then console.log('A') and console.log('C') around loadAnnouncements() in the Load button. In what order does the playground’s Console print them?
function loadAnnouncements() {
get(API + '/announcements', function (err, list) {
console.log('B');
…
});
}
document.getElementById('load').onclick = function () {
start();
console.log('A');
loadAnnouncements();
console.log('C');
loadGrade();
};
Check after you predict
A, C, then B, 2 s later. loadAnnouncements() only starts the call and returns at once, so C prints right away; B waits for the answer. Code written after an asynchronous call runs before its callback. You see A, B, C? Your console.log('B') is outside the callback: it must be inside function (err, list) { … }, the function you give to get.
Put ?fail=1 on the /courses call again. Which callback receives the error, and what if it forgot its if (err)?
What happened
Only the callback of /courses receives the error. Without its if (err), courses is undefined and courses[0] throws inside the callback; no try/catch outside can catch it: the line says “Uncaught error”, and the card waits forever.
In the callback of /me, right after showStudent(me) (before the call for the courses), add the loop below, which keeps the page busy for 3 seconds. Load: what do the clock, the line and the grade card do?
// 3 s of work: the page can do nothing else
const t = Date.now();
while (Date.now() - t < 3000) {}
What happened
The clock stops for 3 s, 2 s after Load, and the line says “page frozen 3.0 s”: your callback holds the page’s one thread, and nothing else can run, not even the other callbacks (the announcements answer, if it arrives meanwhile, waits too). The call for the courses leaves only when your loop ends, at 5 s, so the grade comes at about 9 s instead of 6. In the monitor the load splits into two runs: for 3 s the server had nothing to do. Waiting for an answer frees the page; your own long code never does. Node.js has the same single thread: you will meet it again in part 2. The clock stopped right at the click, and the monitor shows one normal run? Your loop is outside the callback: it ran during the click itself, and froze the page before any answer came.
Solution
// ---------- Your code: callbacks ----------
function get(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true); // true: do not wait, call me back
xhr.onload = function () {
if (xhr.status !== 200) return callback(new Error('HTTP ' + xhr.status));
callback(null, JSON.parse(xhr.responseText));
};
xhr.onerror = function () { callback(new Error('network error')); };
xhr.send(); // returns at once
}
function loadAnnouncements() {
get(API + '/announcements', function (err, list) {
if (err) return showError('announcements', err.message);
showAnnouncements(list);
});
}
function loadGrade() {
get(API + '/me', function (err, me) {
if (err) return showError('grade', err.message);
showStudent(me);
get(API + '/students/' + me.id + '/courses', function (err, courses) {
if (err) return showError('grade', err.message);
showCourse(courses[0]);
get(API + '/students/' + me.id + '/courses/' +
courses[0].id + '/grade', function (err, grade) {
if (err) return showError('grade', err.message);
showGrade(grade);
});
});
});
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
Promises
My code in Web Playground ↗Page broken? Repair it (keeps my code) ↗A promise is an object for an answer that will come later. Replace your get(url, callback) with get(url), XHR wrapped in a promise: return new Promise(function (resolve, reject) { … }), resolve(data) when it works, reject(error) when it fails. loadAnnouncements() and loadGrade() become chains: get(url).then(…), with one .catch at the end. Then write it with fetch(url): its promise gives a response, not the data; check response.ok, then response.json() reads the body and returns a promise of the data.
Same timings as callbacks? What does loadGrade() look like now?
- Return the next call from
.then: the next.thenwaits for it. - The
/gradecall needsme.id, but the second.thenonly receives the courses: declarelet me;before the chain, name the first.then’s parameterstudent, and writeme = student;. A parameter namedmewould hide thatlet. - One
.catchat the end catches an error from any step of the chain. fetch(url)gives a promise of the response, not the response:.json()goes inside its.then,return fetch(url).then(function (response) { … return response.json(); });. “fetch(...).json is not a function” means that.thenis missing.fetch’s promise is rejected only when no answer can be read (network down, server not found, blocked by CORS). On a404or a500it is resolved: checkresponse.okandthrowyourself.
Check after you predict
The same 6 s, the page alive, the same monitor: promises change the code, not the waiting. loadGrade() is flat now: each .then returns the next call, and a single .catch handles the three levels.
In the fetch version, put ?fail=1 on /courses and remove the response.ok line. Then put it back, take ?fail=1 off, and remove the return before the /courses call. Which .then fails, and what does the card say?
What happened
Without response.ok, courses is the error object: the .then that receives it fails with a message about title, not about the 500, so the cause is hidden. Without the return, the next .then runs at once with undefined: the card says “Cannot read properties of undefined (reading '0')”, and in the monitor courses is called, but grade never is.
Solution
// ---------- Your code: XHR in a promise ----------
function get(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function () {
if (xhr.status !== 200) return reject(new Error('HTTP ' + xhr.status));
resolve(JSON.parse(xhr.responseText));
};
xhr.onerror = function () { reject(new Error('network error')); };
xhr.send();
});
}
function loadAnnouncements() {
return get(API + '/announcements')
.then(showAnnouncements)
.catch(function (err) { showError('announcements', err.message); });
}
function loadGrade() {
let me;
return get(API + '/me')
.then(function (student) {
me = student;
showStudent(me);
return get(API + '/students/' + me.id + '/courses');
})
.then(function (courses) {
showCourse(courses[0]);
return get(API + '/students/' + me.id + '/courses/' +
courses[0].id + '/grade');
})
.then(showGrade)
.catch(function (err) { showError('grade', err.message); });
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
function get(url) {
return fetch(url).then(function (response) {
// a 500 does not reject
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
});
}
// ---------- Your code: fetch ----------
function get(url) {
return fetch(url).then(function (response) {
// a 500 does not reject
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
});
}
function loadAnnouncements() {
return get(API + '/announcements')
.then(showAnnouncements)
.catch(function (err) { showError('announcements', err.message); });
}
function loadGrade() {
let me;
return get(API + '/me')
.then(function (student) {
me = student;
showStudent(me);
return get(API + '/students/' + me.id + '/courses');
})
.then(function (courses) {
showCourse(courses[0]);
return get(API + '/students/' + me.id + '/courses/' +
courses[0].id + '/grade');
})
.then(showGrade)
.catch(function (err) { showError('grade', err.message); });
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
async/await
My code in Web Playground ↗Page broken? Repair it (keeps my code) ↗
An async function can await a promise: the function pauses there until the answer arrives, while the page goes on. try/catch handles the errors, as in the synchronous version. Write get(url) with await fetch(url), then make loadAnnouncements() and loadGrade() async: an await before each get(...), inside a try/catch.
The code will look almost like the synchronous version. 8 s like it, or 6 s?
awaitworks inside anasyncfunction only (or at the top level of a module; the playground runs a classic script).get(url)can beasynctoo:await fetch(url), checkresponse.ok, thenreturn response.json().- One
tryaround the threeawaitlines catches an error from any of them. - Each
get(...)needs its ownawait: without it you hold a promise,courses[0]isundefined, and the card talks about'title'.
Check after you predict
The code reads top to bottom like the synchronous version, yet the clock never stops: await pauses loadGrade(), not the page. The dashboard takes 6 s, like with promises: async/await is promises underneath.
Add two lines: console.log('grade shown') after showGrade(grade), and console.log('Load is over?') as the last line inside the Load button’s function, just before its };. Which one prints first, and when?
async function loadGrade() {
try {
…
showGrade(grade);
console.log('grade shown');
} catch (err) {
…
}
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
console.log('Load is over?');
};
Check after you predict
“Load is over?” first, right after the click; “grade shown” about 6 s later. await pauses loadGrade(), not the Load button that called it: the button got a promise back and carried on. The code looks synchronous; it is not.
Remove the await before get(API + '/me'). What is me now, which address does the next line call, and what does the server answer?
What happened
me is a promise, not the student: the card shows “undefined (undefined)”, the next call asks for /students/undefined/courses, and the server answers 404, which catch shows as HTTP 404. In the monitor, me and courses start together: without await, nothing waited.
Solution
// ---------- Your code: async/await ----------
async function get(url) {
const response = await fetch(url);
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}
async function loadAnnouncements() {
try {
showAnnouncements(await get(API + '/announcements'));
} catch (err) {
showError('announcements', err.message);
}
}
async function loadGrade() {
try {
const me = await get(API + '/me');
showStudent(me);
const courses = await get(API + '/students/' + me.id + '/courses');
showCourse(courses[0]);
const grade = await get(API + '/students/' + me.id + '/courses/' +
courses[0].id + '/grade');
showGrade(grade);
} catch (err) {
showError('grade', err.message);
}
}
document.getElementById('load').onclick = function () {
start();
loadAnnouncements();
loadGrade();
};
The await trap
My code in Web Playground ↗Page broken? Repair it (keeps my code) ↗
One line must wait for both cards: console.log('Dashboard ready'). An async function returns a promise at once, resolved when its body ends: that is what a caller can await. Make the Load button async and await both loads:
document.getElementById('load').onclick = async function () {
start();
await loadAnnouncements();
await loadGrade();
console.log('Dashboard ready');
};
How long does the dashboard take now?
Check after you predict
8 s again, though the page stays alive: loadGrade() starts only when loadAnnouncements() is over, and the two do not depend on each other. In the monitor, the bars are back in line. The monitor draws this run like the synchronous one: the server sees only the order of the calls. Your page tells them apart: the clock kept turning, and the line says “page alive”. In line is not frozen.
Back to 6 s, without touching loadGrade() or loadAnnouncements(). Work in your own tab.
- Called without
await,loadAnnouncements()andloadGrade()each start their calls and give back a promise at once. Start both first; wait only after. - Keep the two promises:
const a = loadAnnouncements();andconst g = loadGrade();, thenawait a;andawait g;. Promise.all([…])is the short form: give it both promises, and it waits for all of them.- Inside the brackets, call both loads:
loadAnnouncements()andloadGrade(), with their(). Without them,Promise.allgets two functions, not promises: nothing is called, “Dashboard ready” prints at once, the line says “No call went out”, and the monitor stays empty..then(showGrade)takes a function, to call later;Promise.alltakes promises: calls already started.
Solution
Start both, then wait. Called without await, each load starts its calls and returns its promise at once; the two run side by side, and each await only waits for what is left: 6 s.
document.getElementById('load').onclick = async function () {
start();
// both start now
const a = loadAnnouncements();
const g = loadGrade();
await a;
await g;
console.log('Dashboard ready');
};
Promise.all says it in one line: it waits until every promise you give it is resolved. Each load here catches its own errors, so its promise is always resolved, and “Dashboard ready” prints even when a card fails. A load that does not catch them: break it, below.
document.getElementById('load').onclick = async function () {
start();
await Promise.all([loadAnnouncements(), loadGrade()]);
console.log('Dashboard ready');
};
6 s, and announcements sits beside me in the monitor again. Promise.all is more than shorter: it watches both promises from the start. With await a; await g;, if g fails while you are still waiting for a, the browser reports an uncaught error, even inside a try. Use await where a call needs the one before (me, then courses); between calls that do not, it only makes you wait in line. Promise.all is the right tool when the next line needs both answers: const [list, me] = await Promise.all([get(API + '/announcements'), get(API + '/me')]);
Replace loadAnnouncements() with this version, which catches nothing and asks for ?fail=1, and keep the Promise.all Load button. Does the grade card stop? Does “Dashboard ready” print?
async function loadAnnouncements() {
showAnnouncements(await get(API + '/announcements?fail=1'));
}
What happened
The grade card still fills at 6 s, and the monitor shows its three calls to the end: a rejection cancels nothing. Promise.all is rejected at 2 s, when the announcements fail, so only the await gives up: “Dashboard ready” never prints, and the line says “Uncaught error”, because nothing catches that rejection. To wait for every load whatever happens: await Promise.allSettled([…]). It is never rejected and gives one result per promise, { status: 'fulfilled', value } (resolved) or { status: 'rejected', reason } (rejected). It shows nothing by itself: with allSettled alone, the announcements card keeps “…”, and 2 s after the last answer the line says a card still waits, because only a catch calls showError. Read the results instead, as below: the card shows HTTP 500 and the line ends.
document.getElementById('load').onclick = async function () {
start();
const results = await Promise.allSettled([loadAnnouncements(), loadGrade()]);
if (results[0].status === 'rejected') {
showError('announcements', results[0].reason.message);
}
console.log('Dashboard ready');
};
Quick check · 5 questions
xhr.open('GET', url, false); xhr.send(); The server takes 2 s. What happens to the page meanwhile?
See the answer
false makes send() wait for the answer, and while it waits the page’s main thread runs nothing else. The keystrokes are kept and handled at the end, all at once.
With callbacks, where must the /courses call start?
See the answer
On the next line, the answer has not arrived yet: me does not exist. A setTimeout guesses, and a slow day breaks it. The callback is the one place where the answer is known.
The server answers 500. What does the promise of fetch(url) do?
See the answer
For fetch, an answer is an answer, even a 500: its promise is rejected only when no answer can be read. Check response.ok and throw yourself.
const a = await getA(); const b = await getB(); The two calls do not depend on each other and take 2 s each. How long in total?
See the answer
The second line starts when the first is over. The page lives (not D), but the calls wait in line. await Promise.all([getA(), getB()]): 2 s.
The Load button calls loadGrade() without await. Inside it, await get(url) waits 2 s. What waits?
See the answer
await pauses the function it is written in and hands the thread back to the page; the function resumes when the promise settles. The button, without await, does not wait: it gets a promise and carries on. With await it would wait too, and two await in a row put the calls in line: the trap of section 06. Chapter 13 (coming soon) shows what runs in the meantime.
Recap
- Synchronous XHR,
open(…, false), freezes the page until the answer: never on a real page. - Asynchronous XHR,
open(…, true), lets the page live; your code runs in a callback when the answer arrives. The page still has one thread: a long callback freezes it too. - Calls that need each other nest their callbacks into a pyramid, with an error check at every level.
- A promise turns the pyramid into a chain: each
.thenreturns the next call, one.catchhandles them all.fetchresolves even on a500: checkresponse.ok. awaitpauses the function, not the page. Calls that do not need each other: start them all, then wait for them together withPromise.all.
Sources
- WHATWG, XMLHttpRequest Standard: the
open()method (itsasyncargument, and the note that synchronous XHR outside workers is being removed from the web platform); MDN: XMLHttpRequest.open(). - MDN: Introducing asynchronous JavaScript (callbacks, “callback hell”); Using promises.
- MDN: fetch() (it rejects on a network error, not on an HTTP error status); Response.ok.
- MDN: async function; await; Promise.all(); Promise.allSettled().