-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
143: The Node.js Event Loop vs the Browser's
By now, you're comfortable with the general idea that JavaScript is single-threaded and uses an event loop to handle asynchronous tasks. But here is the secret: there is no single "JavaScript Event Loop." The event loop is an implementation detail of the environment. Chrome's V8 engine in the browser handles the loop differently than Node.js does on your server, even though they both execute the same language.
To really see this in action, I want us to build a small "Execution Order Logger." We'll write one piece of code and run it in both environments to see where they diverge. If you've ever wondered why your setTimeout(fn, 0) didn't fire exactly when you expected, this is where the answer lies.
Establishing a common baseline
Let's start with something that behaves almost identically in both the browser and Node. I'll mix a synchronous log, a macrotask (the timer), and a microtask (the promise).
console.log('🚀 Start');
setTimeout(() => {
console.log('⏲️ Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('📦 Promise');
});
console.log('🏁 End');
If you run this in your browser console or via node index.js, you'll get the same result: Start, End, Promise, Timeout. This is because both environments prioritize the Microtask Queue (Promises) over the Macrotask Queue (Timers) once the current call stack is clear. So far, so good.
Introducing the Node.js specifics
Now, let's add some tools that only exist in Node.js. I'm talking about setImmediate() and process.nextTick(). These are the parts that usually trip people up because they sound like they do the same thing as setTimeout or Promises, but they hook into different phases of the Node loop.
console.log('🚀 Start');
setTimeout(() => console.log('⏲️ Timeout'), 0);
setImmediate(() => console.log('⚡ Immediate'));
process.nextTick(() => console.log('⏭️ NextTick'));
Promise.resolve().then(() => console.log('📦 Promise'));
console.log('🏁 End');
When I first started with Node, I assumed process.nextTick was just another way to write a Promise. I thought it would just go into the microtask queue and they'd execute in the order they were called. I was wrong.
The mistake I made with the Microtask Queue
In my head, I expected the output to be NextTick then Promise, or vice versa. But when I ran this, I saw that process.nextTick always wins. It doesn't just join the microtask queue; it actually tells Node, "Finish whatever you're doing right now, and then execute this before you even look at the promise queue."
If you run the code above in Node, you'll see: Start, End, NextTick, Promise. The nextTick queue is processed immediately after the current operation, before the Promise microtask queue. It's like a "fast pass" at a theme park.
The battle between Timeout and Immediate
Now, look at the setTimeout and setImmediate. If you run this script in a standalone file, you might notice something frustrating: the order between "Timeout" and "Immediate" is actually non-deterministic. Sometimes Timeout wins; sometimes Immediate wins.
Why? Because setTimeout(0) depends on the system clock's precision. If the loop enters the "timers" phase and 1ms has passed, it fires. If not, it moves on to the "poll" phase, and setImmediate (which lives in the "check" phase) will fire first.
However, if we wrap these inside an I/O cycle—like reading a file—the behavior becomes predictable. I'll modify our logger to run inside a fs.readFile callback:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('⏲️ Timeout'), 0);
setImmediate(() => console.log('⚡ Immediate'));
});
In this case, setImmediate will always fire first. This is because the readFile callback executes in the "poll" phase. The very next phase in the Node.js loop is the "check" phase, where setImmediate lives. The "timers" phase is way back at the start of the next loop iteration. This is a fundamental difference from the browser, which doesn't have these distinct phase-based buckets.
📋 Practical Task
Exercise: Predicting the Node.js Execution Race
You are debugging a Node.js application where a developer attempted to ensure a specific cleanup function runs as soon as possible, but before the next event loop tick. They've provided you with the following snippet. Your goal is to determine the exact order of the console logs and explain why process.nextTick overrides the Promise.
const fs = require('fs');
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
process.nextTick(() => console.log('D'));
fs.readFile(__filename, () => {
console.log('E');
setImmediate(() => console.log('F'));
setTimeout(() => console.log('G'), 0);
});
console.log('H');
Requirements:
- Write out the final sequence of letters (A-H) as they will appear in the terminal.
- Identify which letters are part of the "Microtask" phase and which are part of the "Timer" or "Check" phases.
- Explain why 'D' appears before 'C' despite both being asynchronous callbacks triggered at the start of the script.
There are no comments for now.