The event loop and asynchronous JavaScript
JavaScript runs on a single thread, yet pages fetch data, run timers and respond to clicks at the same time. Step through the event loop — call stack, Web APIs, microtasks and tasks — then write clean asynchronous code with promises and async/await.
In this lecture you will
- Explain why JavaScript is single-threaded and what blocking the main thread does
- Trace code through the call stack, Web APIs, the microtask queue and the task queue
- Predict the output order of timers, promises and synchronous code
- Write asynchronous code with promises and async/await, including error handling
A web page is constantly juggling: downloading data, running timers, animating and responding to clicks. Yet JavaScript in the browser runs on a single thread — it can only execute one piece of code at a time. The mechanism that makes this work is the event loop, and understanding it explains a lot of confusing behaviour.
One thread, one thing at a time
JavaScript executes on the browser’s main thread, the same thread that handles user input and rendering. While your code runs, nothing else on the page can happen. That is why a heavy calculation in a click handler makes the whole page freeze.
So how does a page fetch data without freezing? By not waiting. Slow operations — timers, network requests, reading files — are handed to the browser, which does the waiting elsewhere. When the result is ready, a callback is queued, and the event loop runs it when the thread is free.
The four places code waits
- Call stack
- The functions running right now. The event loop only takes new work when the stack is empty.
- Web APIs
- Browser machinery that handles timers, network requests and events while your code does other things.
- Task queue
- Callbacks from timers, events and I/O, waiting their turn. One task runs per loop turn.
- Microtask queue
- Callbacks from resolved promises (and queueMicrotask). The whole queue is emptied after each task, before the next task.
Step through the loop
This small script is a favourite interview question. Before stepping through, predict the order of the output.
- Run the scriptThe whole script is itself a task. console.log(’A’) runs immediately and prints A.
- setTimeout hands off a timersetTimeout registers a callback with the browser’s timer and returns at once. The script keeps running — nothing waits.
- The timer finishesEven with a 0 ms delay, the callback can’t interrupt running code. It goes into the task queue and waits.
- A promise resolvesPromise.resolve().then(…) schedules its callback in the microtask queue — a separate, higher-priority queue.
- Finish the scriptconsole.log(’D’) prints D. The script task is now finished.
- The stack is emptyNow the event loop can pick the next work. Rule: empty the microtask queue first.
- Microtasks runThe promise callback runs and prints C.
- Then the next taskOnly now does the timer callback run, printing B. Final output: A D C B. Between tasks, the browser may also render a frame.
Promises and async/await
Callbacks nested inside callbacks quickly become unreadable. Promises represent a value that will arrive later, and async/await lets you write promise-based code that reads top to bottom:
async function loadCourses() {
status.textContent = 'Loading…';
try {
const response = await fetch('/api/courses');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const courses = await response.json();
render(courses);
status.textContent = '';
} catch (error) {
status.textContent = 'Could not load courses. Please try again.';
console.error(error);
}
}
Each await pauses this function — not the whole page — until the promise settles. Meanwhile the event loop keeps handling clicks and rendering. When the promise resolves, the rest of the function continues as a microtask.
To run independent requests in parallel instead of one after another, start them all and wait once:
const [profile, orders] = await Promise.all([fetch('/api/me'), fetch('/api/orders')]);
Compare the three ways of waiting for the same three requests, then make one of them fail:
The requests are real promises that settle after a delay. Sequential awaits add up; Promise.all takes as long as the slowest request but rejects on the first failure; Promise.allSettled always waits for every result and reports each one’s status.
Keeping the page responsive
- Keep tasks short. Browsers treat tasks longer than about 50 ms as “long tasks” that hurt responsiveness (the INP metric from lecture 1).
- Split big jobs into chunks and yield between them, so input and rendering can happen in the gaps.
- Move heavy computation off the main thread with a Web Worker, which runs JavaScript on a separate thread and communicates by messages.
Feel the difference. Each button runs one second of real busy work. Watch the pulse and the frame timeline, and try clicking while the work runs:
The first button genuinely freezes this page for a second: nothing can paint or respond, and your click waits until the task ends. Chunking lets frames and input slip in between tasks; a worker keeps the main thread completely free. Frame times come from requestAnimationFrame.
That completes the first module: you can now follow a page from the network to the screen and explain how it reacts to users. Module 2 builds modern interfaces on this foundation.
Key takeaways
- The main thread runs one piece of JavaScript at a time; a long task freezes clicks, scrolling and rendering.
- Slow operations are handed to the browser, and their callbacks are queued to run later.
- After each task, all queued microtasks (promise callbacks) run before the next task (such as a timer callback).
- async/await makes asynchronous code read top to bottom — wrap awaits in try/catch to handle failures.
Check your understanding
3 questions · answers explainedQ1What does this log? console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);
Q2A button handler runs a loop that takes 3 seconds. What does the user experience?
Q3How should you handle a failed fetch inside an async function?