Skip to Content
Course content

55: The Event Loop Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why doesn't setTimeout(callback, 0) actually run immediately?

This is usually the first "wait, what?" moment for most developers. You'd think telling JavaScript to wait 0 milliseconds means "do this right now," but that's not how the engine works. JavaScript is single-threaded, meaning it can only do one thing at a time. It has a Call Stack, and as long as there is something on that stack, the engine won't look anywhere else.

When you call setTimeout, you aren't actually telling JS to run that function in 0ms; you're telling the browser (the Web API environment) to start a timer and, once it's done, push the callback function into the Task Queue. The Event Loop's only job is to look at the Call Stack and the Task Queue. If the stack is empty, it pushes the first thing from the queue onto the stack.

console.log('Start');

setTimeout(() => {
  console.log('Timer finished');
}, 0);

console.log('End');

// Output:
// Start
// End
// Timer finished

In the example above, the setTimeout callback is sent to the queue, but it has to wait its turn. The "End" log is still on the main execution stack, so the Event Loop can't move the timer callback up until the script has completely finished running.

What's the deal with Microtasks versus Macrotasks?

If you start working with Promises, you'll notice they behave differently than setTimeout. This is because JavaScript maintains two different queues: the Macrotask Queue (where timers and I/O live) and the Microtask Queue (where Promise callbacks and MutationObserver live).

Here is the golden rule: The Event Loop will prioritize the entire Microtask Queue over the Macrotask Queue. It will not even look at the next setTimeout until every single pending microtask has been cleared. I've seen people accidentally create infinite loops by recursively calling microtasks, which completely freezes the browser because the Macrotask queue (which handles things like UI rendering) never gets a turn.

console.log('Script start');

setTimeout(() => {
  console.log('Macrotask: Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Microtask: Promise 1');
}).then(() => {
  console.log('Microtask: Promise 2');
});

console.log('Script end');

// Output:
// Script start
// Script end
// Microtask: Promise 1
// Microtask: Promise 2
// Macrotask: Timeout

Notice how the Promise chain finishes entirely before the timeout fires, even though the timeout was scheduled "first."

If JS is single-threaded, how does it handle async work without freezing?

It's a common misconception that JavaScript "does" the async work. It doesn't. The JavaScript engine (like V8) just handles the execution. The actual waiting—whether it's a network request via fetch, a timer, or listening for a click—happens outside the engine in the browser's Web APIs (or in Node.js's C++ APIs).

Think of the JS engine as a chef in a kitchen. The chef can't chop vegetables and wait for the oven to preheat at the same time. Instead, the chef tells the oven (the Web API), "Let me know when you're at 400 degrees," and goes back to chopping. When the oven beeps, it places a "Notification" in the Task Queue. The chef finishes the current vegetable they are chopping (clearing the stack) and then checks the queue to see that the oven is ready.

This is why your UI doesn't freeze when you fetch data from an API. The request is handled by the browser's network stack in the background. Your JS code just provides the "callback" (via .then() or await) that the Event Loop will eventually execute once the response comes back.




📋 Practical Task

Exercise: The Execution Order Predictor

You are debugging a piece of legacy code where the execution order is critical. Your goal is to modify the provided code so that the "Final Cleanup" log happens last, and the "Data Processed" log happens before the "UI Updated" log.

Currently, the code is out of order. You must move the logic into the correct types of asynchronous tasks (Microtasks vs Macrotasks) to achieve the desired sequence.


// CURRENT BROKEN CODE:
console.log('Application Start');

setTimeout(() => {
  console.log('UI Updated');
}, 0);

Promise.resolve().then(() => {
  console.log('Final Cleanup');
});

setTimeout(() => {
  console.log('Data Processed');
}, 0);

console.log('Application End');

/* 
  DESIRED OUTPUT:
  Application Start
  Application End
  Data Processed
  UI Updated
  Final Cleanup
*/

Requirements:

  • You cannot change the text inside the console.log statements.
  • You must use a combination of Promise.resolve().then() and setTimeout() to manipulate the priority.
  • Ensure "Final Cleanup" is the absolute last thing to print.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.