Learning Goals
3 minBy the end of this lesson you will be able to:
- Name the three states a promise moves between, and say which are final.
- Chain
.then()calls flat instead of nesting callbacks. - Catch a failure with
.catch()and always tidy up with.finally(). - Build your own promise with
resolve, and watch it settle.
Warm-Up · The Word You Have Been Ignoring
5 minThis was the Warm-Up two lessons ago. You were told to park the answer. Time to collect it.
const answer = fetch(url);
console.log(answer);
console.log(typeof answer);Predict together
- What is the word inside the angle brackets, and what does it mean?
- Is a promise a special language feature, or an ordinary object?
Reveal
Promise { <pending> }
objectPending means “no answer yet”. And typeof says object — a promise is just an object, with methods you can call. Nothing magic.
New Concept · Three States, One Direction
12 minYou take a numbered ticket at the post office. While you wait, the ticket promises nothing except that you will be dealt with. In the end you are either served, or told the counter has closed.
A promise is that ticket. It holds one of three states, and once it leaves the first one it can never go back.
- Pending — no answer yet. Every promise starts here.
- Fulfilled — it worked, and there is a value.
- Rejected — it failed, and there is a reason.
Fulfilled and rejected are called settled. A settled promise is finished for good — it cannot change its mind, and it cannot settle twice.
Handling both endings
fetch(url)
.then((response) => response.json())
.then((data) => {
output.textContent = data.current.temperature_2m;
})
.catch((error) => {
output.textContent = 'Could not load the weather.';
console.log(error.message);
})
.finally(() => {
loading.textContent = '';
});.then()— runs when it is fulfilled..catch()— runs when it is rejected, anywhere above it..finally()— runs either way. Perfect for hiding a spinner.
Why the chain is flat
Here is the rule that makes everything work: every .then() returns a new promise. Whatever you return inside one arrives in the next.
Nested callbacks (Lesson 19)
fry('roti canai', () => {
serve('table 4', () => {
bill('table 4', () => {
// and on it goes
});
});
});Each step buried deeper than the last.
A promise chain
fry('roti canai')
.then(() => serve('table 4'))
.then(() => bill('table 4'))
.catch(handleProblem);Every step at the same level, read top to bottom.
This is the pain promises were invented to remove. One .catch() at the bottom covers every step above it, too — you do not check for failure four separate times.
Making one yourself
You rarely need to, but building one makes the states real. Here is a tidy replacement for setTimeout:
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(2000).then(() => {
console.log('Two seconds later');
});resolve is a function you call to move the promise from pending to fulfilled. There is a reject alongside it for the other ending.
Why it matters
Promises are the shape of every slow thing in JavaScript: network requests, timers, file reads, camera access. Learn the three states once and all of them behave the same.
Worked Example · A Chain That Behaves
12 minRebuild the weather page so it handles a bad ending as gracefully as a good one.
Step 1 — Watch a promise settle
const promise = fetch(url);
console.log(promise);
promise.then(() => {
console.log(promise);
});Promise { <pending> }
Promise { <fulfilled>: Response }The same object, printed twice. Between the two logs it settled and took on a value.
Step 2 — Add the loading state
load.addEventListener('click', () => {
status.textContent = 'Loading...';
fetch(url)
.then((response) => response.json())
.then((data) => {
output.textContent = `${data.current.temperature_2m} °C`;
});
});Add <p id="status"></p> to your HTML. It works — but if anything fails, “Loading…” sits there forever.
Step 3 — Catch the failure
fetch(url)
.then((response) => response.json())
.then((data) => {
output.textContent = `${data.current.temperature_2m} °C`;
})
.catch((error) => {
output.textContent = 'Sorry, could not reach the weather service.';
console.log(error.message);
});Test it properly: turn your wifi off and click. The friendly message appears instead of a silent nothing.
Note where .catch() sits — at the bottom. It catches a rejection from any step above it.
Step 4 — Always tidy up
.catch((error) => {
output.textContent = 'Sorry, could not reach the weather service.';
console.log(error.message);
})
.finally(() => {
status.textContent = '';
});What changed? “Loading…” now clears on both paths. Without .finally() you would write that same line twice, and one day forget one.
Step 5 — Prove the chain passes values along
fetch(url)
.then((response) => response.json())
.then((data) => data.current.temperature_2m)
.then((temperature) => {
console.log(temperature);
});29.8The middle .then() returns a plain number, and the next one receives it. A returned value becomes the next promise's value.
Forget that return and the next step gets undefined. It is the single most common promise bug — and the Mini-Challenge below.
Try It Yourself
13 minWork in your weather page.
Task 1 — A deliberate failure
Change the domain to something that does not exist, such as
https://api.open-meteo.invalid/v1/forecast, and click.Confirm your
.catch()runs and.finally()still clears the status. Then put the real URL back.Task 2 — Slow it down on purpose
Use the
wait()promise from the New Concept to pause two seconds before fetching, so you can actually see “Loading…”.Chain it:
wait(2000).then(() => fetch(url))and carry on from there.Task 3 — Two cities, one after the other
Fetch Kuala Lumpur, show it, then fetch Penang and show that underneath — as one flat chain, with a single
.catch()at the end.Remember to
returnthe secondfetchfrom inside its.then(), or the chain will not wait for it.
🔥 Mini-Challenge · The Chain That Loses Its Data
8 minArjun's page shows undefined °C, and when he unplugs the network nothing is caught. Find three mistakes.
// arjun-weather.js — buggy
const output = document.querySelector('#output');
const status = document.querySelector('#status');
status.textContent = 'Loading...';
fetch(url)
.catch((error) => {
output.textContent = 'Something went wrong.';
})
.then((response) => {
response.json();
})
.then((data) => {
output.textContent = `${data.current.temperature_2m} °C`;
status.textContent = '';
});It works if: the temperature appears, the status clears on both good and bad endings, and pulling the network shows the friendly message.
Reveal the answer
Mistake 1 — the missing return.
{ response.json(); } calls the method and throws the result away. A block-bodied arrow function returns undefined unless you say return. So the next .then() receives nothing — hence undefined °C.
Either return response.json(); or drop the braces: (response) => response.json(). The short form is why you rarely see this bug in the chains above.
Mistake 2 — .catch() is in the wrong place.
It sits above the two .then() calls, so it can only catch the fetch itself. Anything that fails later sails straight past it. A .catch() belongs at the bottom of the chain.
Worse: because the catch handler returns nothing, a failed fetch still continues into the .then() below with undefined — so it breaks twice.
Mistake 3 — the status only clears on success.
status.textContent = '' lives in the last .then(). On a failure it never runs, so “Loading…” stays forever. That is what .finally() is for.
// arjun-weather.js — fixed
status.textContent = 'Loading...';
fetch(url)
.then((response) => response.json())
.then((data) => {
output.textContent = `${data.current.temperature_2m} °C`;
})
.catch((error) => {
output.textContent = 'Something went wrong.';
console.log(error.message);
})
.finally(() => {
status.textContent = '';
});Remember the order: all your .then() calls, then one .catch(), then one .finally().
Recap
3 min- A promise is an ordinary object holding one of three states.
- Pending → fulfilled or rejected. One way, once only.
.then()handles fulfilled ·.catch()handles rejected ·.finally()handles both.- Every
.then()returns a new promise, which is why chains stay flat. - Whatever you
returninside a.then()arrives in the next one. - Forgetting that
returngives the next stepundefined. - Put
.catch()at the bottom — it covers every step above it. new Promise((resolve, reject) => ...)builds one yourself. You will rarely need to.
New words
- Pending — started, no answer yet.
- Fulfilled — finished with a value.
- Rejected — finished with a reason it failed.
- Settled — fulfilled or rejected; either way, final.
Where this is going
Flat is better than nested, but a chain still reads oddly compared with normal code. Next lesson writes the exact same promises in a way that reads straight down the page.
📦 Homework
4 min to brief · ~20 min to doRequired
- Build
countdown.htmlusing thewait()promise: show “3”, wait, “2”, wait, “1”, wait, “Order up!” — as one flat chain of.then()calls. - Compare it with your Lesson 19 version that used four timers. Write two sentences on which is easier to change, and bring both files.
Optional stretch
- Add a
rejecttowait()so it fails if the delay is negative. Catch it and show whyrejectis the other half of the pair. - Look up
Promise.all(). In one sentence: how would it fetch KL and Penang at the same time rather than one after the other?