The Event Loop

The event loop is the mechanism that lets single-threaded Node.js handle many operations at once — it runs your synchronous code first, then processes queued callbacks from timers, I/O, and promises without ever blocking.

Learn The Event Loop in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end of this lesson you'll understand why asynchronous code doesn't run in the order it's written, you'll be able to predict the output of a mix of setTimeout , process.nextTick , and Promises, and you'll know how to keep one thread responsive for thousands of users by never blocking the loop.

What You'll Learn in This Lesson

1️⃣ Synchronous vs Asynchronous

Node runs all of your synchronous code — plain statements, one after another — before it touches anything you scheduled for later. When you call setTimeout , you aren't running code now; you're handing Node a callback to run after the current work finishes. That's why the timer below prints last, even with a delay of 0 .

Read it top to bottom: "1. Start" and "2. End" are synchronous, so they print immediately and in order. The setTimeout callback was queued the moment it was reached, but it can't run until the synchronous code is completely done — so "3. Timeout callback" comes last.

2️⃣ The Ordering: Microtasks Before Macrotasks

Once the synchronous code finishes, Node doesn't pick callbacks at random. It follows a strict priority. Microtasks — process.nextTick first, then resolved Promises — are drained completely. Only then does Node run macrotasks like setTimeout timers and setImmediate . So a Promise scheduled after a timer still runs before it.

Trace it: the two synchronous logs ( A , B ) print first. Then Node drains microtasks — process.nextTick ( C ) outranks the Promise ( D ). Finally the macrotasks run: the timer ( E ) then setImmediate ( F ). The lesson: nextTick before promise before timer .

3️⃣ Why Non-Blocking Matters

The real payoff of the event loop is that Node never sits idle waiting. When you start two slow operations, Node schedules both and keeps running — it doesn't freeze on the first one. They complete in the order they finish , not the order you wrote them. Here a fast task started second finishes before a slow task started first.

Both synchronous lines print first, proving Node didn't block on either timer. Then the 100ms task (B) finishes before the 300ms task (A), even though A appears earlier in the code. This is the whole reason a single Node thread can serve thousands of simultaneous connections: while one request waits on the database, the loop is busy handling others.

Your turn. Predict the order first, then fill in the two ___ blanks so the labels match the expected output.

No blanks this time — just a brief and an outline. Schedule three callbacks so the output appears in the exact order shown. Write it yourself, run it, and check your output against the comments.

📋 Quick Reference — Scheduling in Node

Practice quiz

When does Node run a setTimeout callback relative to your synchronous code?

  • Before the synchronous code
  • Only after the current synchronous code has finished
  • Interleaved with the synchronous code
  • Exactly at the millisecond requested, even mid-code

Answer: Only after the current synchronous code has finished. Synchronous code runs first; queued callbacks like timers run only after it finishes.

Which callback runs first among these microtasks and macrotasks?

  • A resolved Promise .then()
  • A setTimeout(fn, 0)
  • A process.nextTick() callback
  • A setImmediate() callback

Answer: A process.nextTick() callback. process.nextTick is the highest-priority microtask; it runs before Promises and any timer.

Between a resolved Promise .then() and a setTimeout(fn, 0), which runs first?

  • The Promise .then(), because microtasks drain before macrotasks
  • The setTimeout, because it was 0ms
  • Whichever was written first in the file
  • They run at the same time

Answer: The Promise .then(), because microtasks drain before macrotasks. Microtasks (Promises) drain completely before the loop moves to macrotasks like timers.

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

  • 0ms is rounded up to 1000ms
  • It runs immediately in Node, unlike browsers
  • It only runs if there are no other timers
  • It is a macrotask, so it waits for all sync code and microtasks first

Answer: It is a macrotask, so it waits for all sync code and microtasks first. Even at 0ms it is a timer macrotask that waits for synchronous code and the microtask queue.

Is the JavaScript you write in Node single-threaded or multi-threaded?

  • Your JS runs on a single thread, with background threads handling slow I/O
  • Fully multi-threaded with no single thread
  • Single-threaded with no background help at all
  • It depends on the number of CPU cores

Answer: Your JS runs on a single thread, with background threads handling slow I/O. Your JS and the event loop run on one thread; libuv and the OS handle I/O in the background.

Two timers are scheduled: A for 300ms and B for 100ms, with B written second. Which finishes first?

  • A, because it was written first
  • B, because it has the shorter delay
  • They finish together
  • Neither runs without await

Answer: B, because it has the shorter delay. The loop never blocks; callbacks fire in delay order, so the 100ms B finishes before the 300ms A.

Why is non-blocking I/O the key to Node serving thousands of connections on one thread?

  • It uses one thread per connection
  • It blocks until each request is done
  • While one request waits on I/O, the loop keeps handling others
  • It disables the event loop

Answer: While one request waits on I/O, the loop keeps handling others. Because the loop never sits idle waiting, it can handle other work while one request awaits I/O.

What happens if you run a huge synchronous loop on the main thread?

  • Node automatically moves it to another thread
  • It blocks the loop so timers stall and requests wait until it ends
  • It runs as a microtask
  • Nothing changes; the loop is unaffected

Answer: It blocks the loop so timers stall and requests wait until it ends. Heavy synchronous work blocks the single loop, freezing timers and incoming requests until it finishes.

Which order is correct for one tick of the event loop?

  • timers, then Promises, then synchronous code
  • Promises, then synchronous code, then nextTick
  • setImmediate, then synchronous code, then nextTick
  • synchronous code, then microtasks (nextTick then Promises), then macrotasks

Answer: synchronous code, then microtasks (nextTick then Promises), then macrotasks. Each tick runs sync code, drains microtasks (nextTick before Promises), then runs macrotasks.

What is a risk of abusing process.nextTick by queuing it from inside a nextTick?

  • It can starve the loop so timers and I/O never run
  • It speeds up I/O permanently
  • It converts macrotasks to microtasks
  • Nothing; it is always safe

Answer: It can starve the loop so timers and I/O never run. Recursively queuing nextTick can starve the loop, preventing timers and I/O from ever running.