How Node.js Handles Asynchronous Requests (Event Loop)

By Guilherme Luiz Maia Pinto
Picture of the author
Published on
Node.js Event Loop Banner

Node.js is widely known for its efficiency in handling I/O, thanks to its event‑driven model. At the core is the event loop, which enables non‑blocking operations on a single main thread. This article explains how Node.js handles asynchronous requests, outlines event‑loop phases, and shows examples.


What is the Event Loop?

The event loop is the backbone of asynchronous processing in Node.js. The main thread coordinates work while time‑consuming tasks are delegated to the OS or to libuv’s thread pool. The loop progresses through phases each tick:

  1. Timers — callbacks scheduled by setTimeout and setInterval
  2. I/O callbacks — deferred callbacks from completed I/O
  3. Idle/Prepare — internal
  4. Poll — retrieve new I/O events and execute related callbacks
  5. Check — callbacks scheduled with setImmediate
  6. Close callbacks — e.g., socket close events

Microtasks (Promise.then/queueMicrotask) run after each phase, before the loop continues.


Asynchronous Programming in Node.js

Callbacks

const fs = require('fs')

console.log('Starting file read')

fs.readFile('README.md', 'utf8', (err, data) => {
  if (err) return console.error(err)
  console.log('File content length:', data.length)
})

console.log('File reading initiated, but the program continues')

Expected (order):

Starting file read
File reading initiated, but the program continues
File content length: <number>

Promises

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

wait(100).then(() => console.log('Done after 100ms'))

Async/Await

async function run() {
  try {
    const user = await getUser()
    const posts = await getPosts(user.id)
    console.log('Posts:', posts.length)
  } catch (err) {
    console.error('Error:', err)
  }
}

Testing the Event Loop Ordering

console.log('Start')

setTimeout(() => console.log('Timeout'), 0)
setImmediate(() => console.log('Immediate'))
Promise.resolve().then(() => console.log('Promise'))

console.log('End')

Expected output (typical):

Start
End
Promise
Timeout
Immediate

Explanation:

  1. Start/End run synchronously.
  2. The microtask queue (Promises) runs before advancing the loop.
  3. setTimeout(..., 0) executes in the next Timers phase; setImmediate executes in the Check phase of the same tick, so Timeout usually appears before Immediate in scripts scheduled from the main context.

Why is Node.js Fast for I/O?

Node.js excels for I/O‑heavy workloads due to non‑blocking I/O and delegation to the OS/libuv, allowing the single thread to multiplex many concurrent requests. CPU‑bound tasks should be moved to worker threads or separate services.


Conclusion

Node.js efficiently processes asynchronous operations via the event loop. Understanding phases and scheduling rules—callbacks, Promises, and async/await—helps you predict behavior, avoid blocking work, and design high‑throughput services.

Stay Tuned

Want to become a Software Engineer pro?
The best articles and links related to web development are delivered once a week to your inbox.