Why JavaScript is asynchronous at all
This is the module with the reputation, and the reputation is earned — not because the syntax is hard, but because it asks you to give up an assumption you have had since module 2: that code runs top to bottom and a function returns its answer.
Start with the problem, before any of the tools.
One thread
JavaScript does one thing at a time. One call stack, one line running. No other code can run while your loop is looping.
On a page, that thread also draws everything. So while your code runs, nothing else happens — no clicks handled, no scrolling, no typing, no animation.
const start = Date.now();
while (Date.now() - start < 5000) {
// five seconds of work
}
For five seconds the tab is frozen. Buttons do nothing, text cannot be selected, and the browser may offer to kill the page. Everything you built in module 7 stops.
Now think about fetching an order list from a server in Pune, on mobile data. A hundred milliseconds if you are lucky; several seconds if you are on a train. If JavaScript waited for that, the page would freeze every time it asked for anything. That is unacceptable, and avoiding it is the entire reason this module exists.
The way out: do not wait
JavaScript does not wait. It starts the slow thing, carries on with the next line, and deals with the result when it arrives.
console.log('1 before');
setTimeout(() => console.log('3 later'), 0);
console.log('2 after');
1 before
2 after
3 later
setTimeout with a delay of zero still runs last. It is not a queue-jump
instruction; it is "run this when the current work is finished".
That is the shape of every asynchronous operation: network requests, timers, file reads, user events. Start now, finish later, do not block.
How it works: the event loop
Three things, and the picture explains every ordering surprise you will meet.
The call stack — the one thread, running your code.
The task queue — work that is ready to run: a timer that has expired, a click handler, a response that has arrived.
The event loop — a rule: when the stack is empty, take the next thing from the queue and run it.
"When the stack is empty" is the whole thing. Your code runs to completion first.
Only then does anything queued get a turn. A setTimeout(fn, 0) inside a
five-second loop waits the full five seconds.
Two queues, not one, and this is the part that surprises people:
console.log('1 sync start');
setTimeout(() => console.log('4 setTimeout 0'), 0);
Promise.resolve().then(() => console.log('3 promise then'));
console.log('2 sync end');
1 sync start
2 sync end
3 promise then
4 setTimeout 0
Promises go in a separate, higher-priority queue — the microtask queue — and
it is drained completely before a single timer runs. So a promise that is
already resolved still runs after all your synchronous code, but before a
setTimeout(0) queued earlier.
You will not need this daily. You will need it the day something happens in an order you cannot explain.
Who does the actual waiting
If there is one thread, what counts down the timer?
The browser does. setTimeout, fetch and the rest are not JavaScript —
they are browser features, as module 7 said of the DOM. You hand the browser a
job and a callback; the browser does the work outside your thread and queues your
callback when it is done.
So the concurrency is real; it just is not happening in your JavaScript. Node works the same way with the operating system.
This also explains a common disappointment: setTimeout(fn, 100) means "not
before 100ms", not "at 100ms". If the thread is busy when the timer expires,
your callback waits its turn.
The consequence you must accept
Here is the bit that breaks people's mental model:
function getOrders() {
let result;
setTimeout(() => {
result = 'the data';
}, 100);
return result;
}
console.log(getOrders());
undefined
return result runs immediately, long before the callback. The function returned
before the answer existed.
No amount of restructuring fixes this. You cannot return a value that has not arrived. There is no "wait here" that does not freeze the page.
So the pattern has to change. Instead of returning the value, you hand over a function to be called with it — module 3's callbacks, which is the next lesson and the beginning of the whole story:
function getOrders(callback) {
setTimeout(() => {
callback('the data');
}, 100);
}
getOrders((data) => console.log(data));
the data
Everything in this module — callbacks, promises, async/await — is a
progressively nicer way of writing that one idea.
What is and is not asynchronous
A frequent confusion:
| Asynchronous | Synchronous |
|---|---|
fetch |
JSON.parse |
setTimeout, setInterval |
array.map, filter, reduce |
| Event handlers | localStorage.getItem |
.then, await |
Everything in modules 2–6 |
map and filter are not asynchronous. They take a callback, which makes
them look similar, but they run it immediately and finish before the next line.
A callback does not mean "later" — it means "here is a function"; only the thing
you gave it to decides when.
And note localStorage is synchronous, which is why module 7 warned that reading
a large blob on every keystroke makes a phone stutter. It blocks the one thread.
Check your work
JavaScript runs on one thread, which on a page is also the thread that draws it. A long loop freezes the tab entirely.
setTimeout(fn, 0) still runs after all synchronous code. Zero means "as
soon as the current work finishes", not "now".
The event loop takes work from the queue only when the stack is empty.
Promises use a separate, higher-priority microtask queue, drained before
timers — so the order is sync, then .then, then setTimeout.
The browser does the waiting, not JavaScript. setTimeout and fetch are
browser features that queue your callback when they finish.
setTimeout(fn, 100) means "not before 100ms."
A function cannot return a value that has not arrived yet — it returns
undefined. The fix is to hand over a function to be called with the value.
map and filter are synchronous despite taking callbacks. So is
localStorage.
Practice
- Freeze a tab on purpose. Run a five-second
whileloop and try to click a button or select text while it runs. This is what "blocking" means, and experiencing it once explains the rest of the module. - Log three lines around a
setTimeout(fn, 0)and predict the order before running it. - Add a
Promise.resolve().then(...)to that and predict again. Getting this one wrong first is normal. - Put a
setTimeout(fn, 0)before a five-second blocking loop and measure when the callback actually runs. Explain it using the stack and the queue. - Write the
undefinedbug. BuildgetOrders()that sets a value in asetTimeoutand returns it. Confirm you getundefined, then try to fix it by returning later in the function and confirm that you cannot. - Rewrite it to take a callback and confirm the data arrives.
- Use
setIntervalto log a counter every second, then stop it after five withclearInterval. Note that you need to keep the id it returns. - Decide, for each of
fetch,JSON.parse,array.map,addEventListenerandlocalStorage.getItem, whether it is asynchronous. Two of them are commonly guessed wrong.
Next: callbacks — the first solution to this problem, and how it got out of hand.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship