Learning Goals
3 minBy the end of this lesson you will be able to:
- Freeze a page on purpose with a long loop, and explain why the buttons stop responding.
- Predict the order three
console.loglines print when one sits insidesetTimeout. - Show that
setTimeout(fn, 0)still runs last, and say why. - Run steps in a chosen order by passing a callback instead of hoping.
Warm-Up · Three Lines, What Order?
5 minLast lesson you met setTimeout and used it to hide a message after two seconds. Here it is again, with a delay of zero.
console.log('Order taken');
setTimeout(() => {
console.log('Nasi lemak ready');
}, 0);
console.log('Next customer');Predict together
- Which line prints first?
- The delay is
0. Does that mean “right now”?
Reveal
Order taken
Next customer
Nasi lemak readyThe zero-delay line still prints last. So 0 does not mean “now”. It means “as soon as there is nothing else to do”.
Today is about what that sentence really means.
New Concept · One Lane, and a Queue
12 minPicture a mamak stall with one waiter. You order roti canai. He does not stand at the griddle watching it cook — he hands the order to the kitchen and serves the next table. When the roti is ready, the kitchen calls him back.
JavaScript is that waiter. It runs one thing at a time, in one lane. Slow jobs get handed to the browser, and your code carries on.
Blocking: doing the waiting yourself
let total = 0;
for (let i = 0; i < 2000000000; i += 1) {
total += i;
}
console.log('Finished counting');That loop takes a second or two. While it runs, the waiter is stuck at the griddle: buttons do not respond, links do not open, nothing on the page moves. This is called blocking.
Non-blocking: handing the wait over
setTimeout(() => {
console.log('Finished waiting');
}, 2000);
console.log('Still free to work');Here the browser keeps the timer, not your code. The page stays alive for the whole two seconds.
Synchronous
Each line finishes before the next one starts.
Simple to read, top to bottom.
A slow line freezes the whole page.
Asynchronous
The slow part is handed to the browser.
Your code carries on immediately.
The result arrives later, in a callback.
Where “later” actually lives
When a timer finishes, its callback does not barge in. It joins a queue and waits for the lane to be completely empty.
0 puts the callback in the queue straight away — but the queue is still behind everything in the lane.That is the whole answer to the Warm-Up. The delay is a minimum wait, never a promise of an exact moment.
Why it matters
Asking a server for data takes anywhere from a fraction of a second to several seconds. If that waiting blocked the lane, every site would freeze on every load.
Next lesson you meet fetch, which does exactly this handing-over. Everything it does rests on what you have just learned.
Worked Example · Freeze a Page, Then Unfreeze It
12 minYou will build a page that proves when JavaScript is stuck. Make a new folder with waiting.html and app.js.
Step 1 — A page with a heartbeat
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Why Code Waits</title>
</head>
<body>
<h1>Kedai Makan Aisyah</h1>
<button id="tick">Click me</button>
<p id="count">Clicks: 0</p>
<button id="slow">Cook the slow way</button>
<script src="app.js"></script>
</body>
</html>Step 2 — Prove the page is alive
const tick = document.querySelector('#tick');
const count = document.querySelector('#count');
const slow = document.querySelector('#slow');
let clicks = 0;
tick.addEventListener('click', () => {
clicks += 1;
count.textContent = `Clicks: ${clicks}`;
});Nothing new here — this is Lesson 16. Click the button a few times and watch the number climb. That counter is your heartbeat monitor.
Step 3 — Block the lane on purpose
slow.addEventListener('click', () => {
console.log('Cooking...');
let total = 0;
for (let i = 0; i < 2000000000; i += 1) {
total += i;
}
console.log('Cooked');
});Click Cook the slow way, then hammer the Click me button while it runs.
The counter does not move. It is frozen, not broken — and when the loop ends, every click you made lands at once. Those clicks were queued the whole time.
Cooking...
CookedIf your machine is fast and the freeze is too brief to see, add a zero to the loop limit. If it hangs for too long, remove one.
Step 4 — Hand the waiting to the browser
The loop above was a fake wait. Real waiting should never be done by your code. Add a second button to waiting.html:
<button id="polite">Cook the polite way</button>const polite = document.querySelector('#polite');
polite.addEventListener('click', () => {
console.log('Cooking...');
setTimeout(() => {
console.log('Cooked');
}, 2000);
});What changed? Click it, then hammer the counter again. The number keeps climbing for the full two seconds, and Cooked still arrives on time.
Same two seconds, same two messages — but the waiter served the other tables instead of standing at the griddle.
Step 5 — Putting steps in order
Now a trap. You want to fry, then serve. Try it the obvious way:
function fry(dish) {
setTimeout(() => {
console.log(`${dish} is fried`);
}, 1500);
}
fry('roti canai');
console.log('Served');Served
roti canai is friedServed before it was fried. Writing a line underneath something asynchronous does not make it happen afterwards.
The fix is to hand over the “what next” as a function — a callback:
function fry(dish, done) {
setTimeout(() => {
console.log(`${dish} is fried`);
done(); // only now is it safe to continue
}, 1500);
}
fry('roti canai', () => {
console.log('Served');
});roti canai is fried
ServedThis is the same hand-over-a-function idea as addEventListener and setTimeout. Order in asynchronous code comes from nesting, not from line numbers.
Nest three or four of these and it becomes hard to read. That pain is exactly why promises exist — Lesson 22.
Try It Yourself
13 minKeep working in waiting.html and app.js.
Task 1 — Teh tarik in two seconds
Add a button and a paragraph. On click, set the paragraph to “Brewing…” immediately, then to “Teh tarik ready” two seconds later with
setTimeout.Both changes go in the same listener. Check that the first one really does appear straight away.
Task 2 — Predict, then check
Write four
console.loglines: two plain ones, one inside a 1000ms timer, one inside a 0ms timer. Before you run it, write your predicted order in a comment at the top.Open the console (Lesson 12) and compare. If you were wrong, work out which rule you forgot.
Task 3 — A countdown
Make a paragraph show
3, then2, then1, thenOrder up!— one second apart.Start with four separate
setTimeoutcalls. Then try to rewrite it with aforloop from Lesson 7 and an array from Lesson 9. Think carefully about what each delay must be.
🔥 Mini-Challenge · The Countdown That Counts Nothing
8 minWei Jie tried Task 3 with a loop. His paragraph jumps straight to “Order up!” and the console message is plainly wrong. Find three mistakes.
// wei-jie-countdown.js — buggy
const message = document.querySelector('#message');
const steps = ['3', '2', '1', 'Order up!'];
for (let i = 0; i < steps.length; i += 1) {
setTimeout(message.textContent = steps[i], 1000);
}
console.log('Countdown finished');It works if: the paragraph shows 3, 2, 1 and “Order up!” one second apart, and Countdown finished appears in the console only after “Order up!” is on screen.
Reveal the answer
Mistake 1 — the callback is called immediately.
setTimeout(message.textContent = steps[i], 1000) runs the assignment on the spot and hands the resulting string to the timer. A timer given a string has nothing to run later. It is the brackets trap from Lesson 16, wearing a new hat: pass a function, not a result.
Mistake 2 — every timer has the same delay.
All four are set to 1000, so all four fire at roughly the same moment. The delay must grow with the loop: (i + 1) * 1000.
Remember they are all scheduled instantly, within the same millisecond. The delay is measured from now, not from the previous step.
Mistake 3 — “finished” is not finished.
console.log('Countdown finished') is the last written line, so it runs first — before a single step has shown. To mean what it says, it must move inside the last timer's callback.
// wei-jie-countdown.js — fixed
const message = document.querySelector('#message');
const steps = ['3', '2', '1', 'Order up!'];
for (let i = 0; i < steps.length; i += 1) {
setTimeout(() => { // a function, not a result
message.textContent = steps[i];
if (i === steps.length - 1) {
console.log('Countdown finished');
}
}, (i + 1) * 1000); // 1s, 2s, 3s, 4s from now
}The one to remember is Mistake 3. In asynchronous code, the last line of the file is almost never the last thing to happen.
Recap
3 min- JavaScript runs one thing at a time, in a single lane.
- A slow loop blocks that lane, and the whole page freezes with it.
- Clicks made during a freeze are not lost — they queue, then all land at once.
- Slow waiting belongs to the browser. Hand it over and your code carries on.
- A timer callback joins a queue and only runs when the lane is empty.
setTimeout(fn, 0)therefore still runs after every plain line.- A delay is a minimum wait, never an exact moment.
- To make one thing follow another, pass a callback — line order will not do it.
New words
- Synchronous — each line finishes before the next begins.
- Asynchronous — the slow part is handed over, and the result arrives later.
- Blocking — occupying the single lane so nothing else can run.
- Callback — a function you hand over to be called when something is ready.
Where this is going
Every asynchronous thing you meet from here — fetch, promises, async / await, and React's useEffect — is built on this one lane. Next lesson you use it to ask another computer for data.
📦 Homework
4 min to brief · ~20 min to doRequired
- Build
traffic.html: a traffic light that goes red → amber → green → red, two seconds apart, by changing a class on a<div>. Use the CSS transitions from Lesson 18 so the colour glides. - Add a counter button beside it. Prove the page stays responsive during the whole cycle, and screenshot the counter mid-cycle. Bring the file and the screenshot next lesson.
Optional stretch
- Make the light loop forever. Look up
setIntervaland write one sentence on how it differs fromsetTimeout. - Try a delay of
1millisecond and one of0. Do they arrive in the order you wrote them? Note what you observe — you will explain it properly in Lesson 22.