Node.js  

Understanding Event Loop, Callbacks, and Promises in Node.js

Node.js is powerful for building fast, scalable network applications, and much of that power comes from its ability to handle asynchronous operations. If you've ever worked with Node.js, you've likely encountered terms like event loop, callbacks, and promises.

In this article, we'll break these down in simple terms, walk through real code examples, and explain how they all fit together in the world of asynchronous programming.

What is Asynchronous Programming?

JavaScript (and Node.js) is single-threaded, meaning it processes one instruction at a time. If one task takes a long time (like reading a file or making a network request), it could block everything else from running unless we handle it asynchronously.

Asynchronous programming enables Node.js to handle tasks such as file I/O, database queries, and API calls without freezing the entire application.

The Event Loop: Node's Brain

The event loop is a mechanism in Node.js that allows it to perform non-blocking I/O operations. It listens for events and runs tasks from different queues (like timers or promise resolutions).

How does it work?

  1. Executes synchronous code first.
  2. Handles microtasks (like Promise.then()).
  3. Then processes macrotasks (like setTimeout).
  4. Repeats the cycle.

Example

console.log('Start');

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

Promise.resolve().then(() => {
  console.log('Promise callback');
});
console.log('End');

Output

Start
End
Promise callback
Timeout callback

Why?

  • Promise.then() is a microtask, executed right after the synchronous code.
  • setTimeout() is a macrotask, executed after microtasks.

Callbacks: The Classic Asynchronous Tool

What is a Callback?

A callback is a function passed as an argument to another function. It’s executed when the first function completes - often asynchronously.

const fs = require('fs');

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    return console.error(err);
  }
  console.log(data);
});

Callback Hell

As you nest callbacks deeper, code can become hard to manage.

doTask1((res1) => {
  doTask2(res1, (res2) => {
    doTask3(res2, (res3) => {
      console.log(res3);
    });
  });
});

This “pyramid of doom” led to the evolution of promises.

Promises: A Modern Alternative

What is a Promise?

A promise is an object representing the eventual completion or failure of an asynchronous operation.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Success!');
  }, 1000);
});
myPromise
  .then((value) => console.log(value))
  .catch((err) => console.error(err));

Promise States

  • Pending: Initial state.
  • Fulfilled: Operation completed successfully.
  • Rejected: Operation failed.

Promises in Action

function asyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('Task Done'), 2000);
  });
}
asyncTask().then(console.log); // Outputs "Task Done" after 2 seconds

Async/Await: Cleaner Syntax with Promises

Introduced in ES2017, async/await allows you to write asynchronous code like it’s synchronous.

async function fetchData() {
  try {
    const data = await asyncTask();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}
fetchData();

This is still based on promises, but it's easier to read and write.

Event Loop + Promises + Callbacks: Putting It Together

console.log('Start');

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

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

process.nextTick(() => {
  console.log('nextTick');
});
console.log('End');

Output

Start
End
nextTick
Promise
setTimeout

Execution order

  • Synchronous: Start, End
  • process.nextTick() (before promises)
  • Promises
  • Timers like setTimeout

Summary Table

Concept Purpose Queue
Event Loop Runs and manages all tasks          —
Callback Function called after async operation Callback queue
Promise Handles future values (success/failure) Microtask queue
setTimeout Delay execution of a task Macrotask queue
process.nextTick Runs after the current phase, before promises Microtask queue (special)

Conclusion

Node.js handles concurrency not through threads, but through a smart event loop and non-blocking, asynchronous APIs. Understanding callbacks, promises, and the event loop is essential to writing high-performance Node.js applications.

Start simple, experiment with examples, and gradually build your mental model and soon, you’ll write async code with confidence.