CodingNic

Node.js Fundamentals

The Event Loop

Node.js Fundamentals 12 min read

The Event Loop

Objectives

By the end of this lesson, you should be able to:

  • Explain, at a working level, what the event loop does
  • Predict the execution order of synchronous code, microtasks, and macrotasks
  • Explain why this ordering matters for real Node.js code

💡 Why this matters: Every async pattern from Module 2, promises, async/await, timers, is scheduled and run by the event loop. Understanding its ordering explains behavior that otherwise looks unpredictable, like why a promise callback can run before a setTimeout callback even with the same delay.

⚠️ A note on verification: every snippet and output in this lesson was actually run with Node.js.

The Basic Idea

Node.js runs JavaScript on a single thread (previous lesson), but constantly has asynchronous work in flight, timers, file reads, network requests. The event loop is the mechanism that manages all of this: it runs synchronous code first, then continuously checks whether any pending asynchronous work has finished, running the associated callback when it has, in a specific, predictable order.

Synchronous Code Always Runs First

javascript
console.log("1: synchronous start");

setTimeout(() => {
  console.log("4: setTimeout callback (macrotask)");
}, 0);

Promise.resolve().then(() => {
  console.log("3: promise callback (microtask)");
});

console.log("2: synchronous end");
text
1: synchronous start
2: synchronous end
3: promise callback (microtask)
4: setTimeout callback (macrotask)

Even with a 0 millisecond delay, setTimeout’s callback doesn’t run third, it runs last. All synchronous code (console.log("1...") and console.log("2...")) finishes completely before the event loop even looks at any pending callbacks. Between the two async callbacks, the promise’s .then() (a microtask) runs before the setTimeout callback (a macrotask), Node.js always fully drains the microtask queue before moving on to the next macrotask.

nextTick Runs Before Promises

javascript
console.log("start");

process.nextTick(() => {
  console.log("nextTick callback");
});

Promise.resolve().then(() => console.log("promise callback"));

console.log("end");
text
start
end
nextTick callback
promise callback

process.nextTick() is a Node.js-specific mechanism that runs even before the microtask queue (promises), it’s the highest-priority way to defer a callback to “right after the current synchronous code finishes, before anything else.” It’s used sparingly in real code, but explains why nextTick callbacks consistently run ahead of promise callbacks.

Why This Ordering Actually Matters

This isn’t just trivia. Real bugs come from assuming asynchronous code will run in the order it’s written, rather than the order the event loop actually schedules it. A common mistake: expecting a value set inside a setTimeout or a .then() to already be available on the very next line, it won’t be, that next line runs immediately, well before the async callback ever fires. This is exactly why await (Module 2) exists, it’s syntax that correctly pauses execution until an async operation genuinely finishes, rather than assuming it already has.

Try It

  1. Predict the output order of a script with one console.log, one setTimeout(fn, 0), and one Promise.resolve().then(fn), then run it and confirm.
  2. Add a process.nextTick() call to the script from question 1, and predict where it falls in the output order before running it.
  3. Explain, in your own words, why a value assigned inside a setTimeout callback isn’t available on the line immediately after the setTimeout call.
  4. Explain, in your own words, the difference between a microtask (like a promise callback) and a macrotask (like a setTimeout callback), in terms of when each one runs.

Recap

  • All synchronous code runs to completion before the event loop processes any pending async callbacks.
  • Microtasks (promise callbacks) are fully drained before the next macrotask (like a setTimeout callback) runs.
  • process.nextTick() runs even before microtasks, the highest-priority way to defer a callback in Node.js.

Next lesson: global objects, values available everywhere in a Node.js program.