Learning Goals
3 minBy the end of this lesson you will be able to:
- Wrap awaited code in
try/catchso a failure cannot stop the page. - Explain why a
404does not reject, and checkresponse.okyourself. - Raise your own failure with
throw new Error(). - Show the visitor a message they can act on, instead of a blank space.
Warm-Up · Two Failures, One Silence
5 minIn Lesson 20 you tried both of these. One of them upset the console and one did not.
// A — the address is wrong
await fetch('https://api.open-meteo.com/v1/nasi-lemak');
// B — the wifi is off
await fetch('https://api.open-meteo.com/v1/forecast?latitude=3.14');Predict together
- Which one throws, and which one comes back quietly?
- In both cases, what does the visitor see on the page?
Reveal
A does not throw. The server answered — it just said 404. As far as fetch is concerned, the delivery arrived.
B throws. There was no delivery at all, so the promise rejects with TypeError: Failed to fetch.
And the visitor? Both times they see nothing change. Silence is the worst possible answer, and it is what your code does today.
New Concept · Two Kinds of Wrong
12 minYou send a friend to buy nasi lemak. They might never come back — phone dead, bus broken. Or they might come back holding a note that says “sold out”.
Both are failures to you. To fetch, only the first one counts.
Nobody came back
No network, wrong domain, request blocked.
The promise rejects.
catch runs by itself.
They came back with bad news
Status 404, 500, 403.
The promise fulfils.
catch does nothing unless you act.
This surprises everybody once. An HTTP error is a successful conversation with a disappointing answer, so you have to inspect the answer yourself.
try / catch / finally
async function loadWeather() {
try {
const response = await fetch(url);
const data = await response.json();
output.textContent = `${data.current.temperature_2m} °C`;
} catch (error) {
output.textContent = 'Could not load the weather. Please try again.';
console.log(error.message);
} finally {
status.textContent = '';
}
}try— the code that might fail.catch (error)— runs only if something in the try fails.finally— runs either way, for tidying up.
These are the same three jobs as .then(), .catch() and .finally() from Lesson 22 — written as blocks instead of a chain.
Making a bad status count
Since a 404 will not reject on its own, you check ok and raise the failure yourself:
const response = await fetch(url);
if (!response.ok) {
throw new Error(`The server said ${response.status}`);
}
const data = await response.json();throw stops the try block immediately and jumps to catch. Now both kinds of wrong end up in the same place.
new Error('...') builds an error object. The text you pass becomes its message, which is what you read in the console later.
catch, two routes in. The ok check is the bridge for the lower one.What to actually say
The message in catch is for the visitor, not for you. Compare:
- ❌
TypeError: Failed to fetch— meaningless to them. - ❌ A blank space — they think the page is broken.
- ✅ “Could not load the weather. Check your connection and try again.”
Say what happened, and what they can do next. Keep the technical detail in console.log, where it helps you.
Why it matters
Every app you admire has thought about this. A good error message is the difference between a visitor retrying and a visitor leaving.
Worked Example · A Page That Copes
12 minHarden the weather page from last lesson until nothing you do to it produces silence.
Step 1 — Wrap what you have
async function loadWeather() {
try {
const response = await fetch(url);
const data = await response.json();
output.textContent = `${data.current.temperature_2m} °C`;
} catch (error) {
output.textContent = 'Could not load the weather.';
console.log(error.message);
}
}Turn your wifi off and click. The friendly line appears, and the real reason is in the console for you.
Step 2 — Catch the bad status too
const response = await fetch(url);
if (!response.ok) {
throw new Error(`The server said ${response.status}`);
}
const data = await response.json();Now change the path to /v1/nasi-lemak and click. Before this step you got silence; now the same message appears.
The server said 404What changed? Nothing about the request — only that you now inspect the answer instead of trusting it.
Step 3 — Tell them which problem it was
} catch (error) {
if (error.message.includes('Failed to fetch')) {
output.textContent = 'No connection. Check your wifi and try again.';
} else {
output.textContent = 'The weather service is having trouble. Try later.';
}
console.log(error.message);
}Two different problems, two different pieces of advice. Only one of them is worth the visitor checking their router.
Step 4 — Always clear the loading state
} finally {
status.textContent = '';
load.disabled = false;
}Disable the button at the start of loadWeather and re-enable it here. A visitor who cannot click twice cannot start two requests by accident.
Without finally, a failure would leave the button dead forever — a far worse bug than the original.
Step 5 — Try to break it
Work through the list and check the page always says something:
- Wifi off → connection message.
- Path changed to
/v1/nasi-lemak→ service message. - Domain changed to
.invalid→ connection message. - Latitude changed to
999→ check what the API says, and what your page says.
That last one is the interesting case. Some APIs answer a nonsense question with a 400 and an explanation in the body — so your ok check catches it.
Try It Yourself
13 minWork in your hardened weather page.
Task 1 — A retry button
When the catch runs, show a “Try again” button beside the message. Clicking it calls
loadWeather()once more.Hide the button again as soon as a request succeeds.
Task 2 — Guard the menu too
Go back to your stall-menu page from Lesson 21 and give it the same treatment:
try, anokcheck, and a message.Then rename
menu.jsonso it cannot be found, and confirm the page explains itself rather than staying empty.Task 3 — Break the JSON
Put a trailing comma in
menu.json— the Lesson 21 mistake. The file loads fine, sookistrue, but the parse fails.Does your
catchhandle it? Work out why, then write one sentence explaining which line actually threw.
🔥 Mini-Challenge · The Catch That Catches Nothing
8 minMei Ling's page shows undefined °C when the address is wrong, and her spinner never stops. Find three mistakes.
// mei-ling-weather.js — buggy
async function loadWeather() {
status.textContent = 'Loading...';
const response = await fetch(url);
try {
const data = await response.json();
output.textContent = `${data.current.temperature_2m} °C`;
status.textContent = '';
} catch (error) {
output.textContent = error.message;
}
}It works if: a wrong address and a dropped connection both show a message a visitor can understand, and the status clears every time.
Reveal the answer
Mistake 1 — the fetch sits outside the try.
A dropped connection rejects on that line, before try begins. Nothing catches it, so the function stops dead and the spinner runs forever. The try must start above the fetch.
Mistake 2 — no ok check.
A 404 fulfils happily. response.json() may even succeed, giving an object with no current in it — so data.current.temperature_2m is where it finally breaks, two steps from the real problem. Check ok and throw at once.
Mistake 3 — the raw message, and no finally.
Showing error.message puts TypeError: Failed to fetch in front of a visitor, which helps nobody. And clearing the status inside try means it only clears on success.
// mei-ling-weather.js — fixed
async function loadWeather() {
status.textContent = 'Loading...';
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`The server said ${response.status}`);
}
const data = await response.json();
output.textContent = `${data.current.temperature_2m} °C`;
} catch (error) {
output.textContent = 'Could not load the weather. Please try again.';
console.log(error.message);
} finally {
status.textContent = '';
}
}The pattern to memorise: try starts before the first await, the ok check comes straight after the fetch, the visitor gets plain English, and finally tidies up.
Recap
3 mintry/catch/finallyis theasyncversion of Lesson 22's chain.- Start the
trybefore the firstawait, or it guards nothing. - A network failure rejects; an HTTP error status does not.
- Check
response.okandthrow new Error()to route bad statuses into your catch. throwjumps straight out oftryand intocatch.- Show the visitor plain English; keep
error.messagefor the console. - Tell them what to do next — “check your connection and try again”.
finallyclears spinners and re-enables buttons on both paths.
New words
- throw — raise a failure on purpose.
- Error — an object carrying a
messagethat explains a failure. - HTTP error — a real answer with a bad status, such as 404 or 500.
End of Section C
You can now reach any API in the world, read what it sends, write the waiting so it stays readable, and cope when it fails. That is the whole job of talking to a server.
Section D starts React. Everything there — components, state, useEffect — sits on the async foundations you have just finished building.
📦 Homework
4 min to brief · ~20 min to doRequired
- Finish your three-city weather page — Kuala Lumpur, Penang and Johor Bahru — with
async/await, anokcheck, a friendly catch and afinally. - Then break it four ways: wifi off, wrong path, wrong domain, broken JSON. Screenshot what the visitor sees each time, and bring all four.
Optional stretch
- Retry automatically: on failure, wait two seconds with your
wait()promise and try once more before giving up. - Read about
AbortController. In one sentence, how would you stop a request that is taking too long?