Skip to content
Laviyx Learn

Try

Node.js and Express course menu

The Node.js Event Loop Explained (with Examples)

Understand how Node.js runs asynchronous code: the call stack, the event loop and its phases, timers, setImmediate and process.nextTick, promise microtasks, why blocking code freezes a server, running async work in parallel with Promise.all, and worker threads for CPU-heavy tasks.

Intermediate9 min readUpdated
On this page

Node.js runs your JavaScript on a single thread. It handles many things at once not by running code in parallel, but by never waiting: slow operations (disk, network, timers) are handed to the system, and when they finish, their callbacks are queued. The event loop takes queued callbacks one at a time and runs them.

Synchronous first, then queued callbacks#

JavaScript
console.log("start");

setTimeout(() => console.log("timeout callback"), 0);
Promise.resolve().then(() => console.log("promise callback"));
process.nextTick(() => console.log("nextTick callback"));

console.log("end of the script");
Output
start
end of the script
promise callback
nextTick callback
timeout callback

The order follows clear rules:

  1. All the synchronous code runs first, to the end.
  2. Then the microtasks: promise callbacks (.then, code after await) and process.nextTick callbacks.
  3. Then the event loop moves on to timers and other phases.

Microtasks always run before the next timer or I/O callback, even a setTimeout of 0 ms.

nextTick or promise first?#

Inside callbacks (timers, I/O, events) and in CommonJS scripts, the process.nextTick queue runs before promise callbacks:

JavaScript
setTimeout(() => {
  Promise.resolve().then(() => console.log("promise"));
  process.nextTick(() => console.log("nextTick"));
}, 0);
Output
nextTick
promise

The first example printed the promise callback first because it runs at the top level of an ES module: Node evaluates the module's own code as part of a promise job, so promise callbacks queued there run before the nextTick queue is processed. The same code saved as a .cjs file prints "nextTick callback" first. In everyday code you rarely need process.nextTick; prefer promises and queueMicrotask.

The phases of the event loop#

Each turn ("tick") of the loop visits phases in order:

Text
   ┌─► timers ............ setTimeout / setInterval callbacks whose time has come
   │   pending callbacks .. some deferred system callbacks
   │   poll ............... wait for and run I/O callbacks (files, network)
   │   check .............. setImmediate callbacks
   └── close callbacks .... e.g. socket 'close' events
   (between every callback: nextTick queue, then promise microtasks)

Inside an I/O callback, setImmediate always runs before a setTimeout(..., 0), because the check phase comes right after poll:

JavaScript
import { readFile } from "node:fs";

readFile(import.meta.filename, () => {
  setTimeout(() => console.log("timeout"), 0);
  setImmediate(() => console.log("immediate"));
});
Output
immediate
timeout

(At the top level of a script, outside any I/O callback, the order of these two is not guaranteed.)

Blocking the event loop#

Because there is one thread, a long synchronous task stops everything else: no timers fire and no requests are answered until it finishes.

JavaScript
const start = Date.now();
setTimeout(() => console.log(`Timer fired after ${Date.now() - start >= 300 ? "at least 300" : "under 300"} ms (asked for 10 ms)`), 10);

// Simulate heavy CPU work for about 300 ms
while (Date.now() - start < 300) {}
console.log("Heavy work done");
Output
Heavy work done
Timer fired after at least 300 ms (asked for 10 ms)

The timer asked for 10 ms but could only run after the loop finished. In a server, that means every user waits. Keep request handlers quick, use asynchronous APIs, and move heavy computation (image processing, large calculations, big JSON parsing) to worker threads (node:worker_threads), a job queue, or another service.

Running async work in parallel#

Async functions that do not depend on each other should run at the same time. Awaiting them one by one wastes time:

JavaScript
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const getUser = () => wait(200, { name: "Asha" });
const getOrders = () => wait(200, [101, 102]);

let t = Date.now();
const user1 = await getUser();
const orders1 = await getOrders();
console.log(`One after another: ~${Math.round((Date.now() - t) / 100) * 100} ms`);

t = Date.now();
const [user2, orders2] = await Promise.all([getUser(), getOrders()]);
console.log(`In parallel: ~${Math.round((Date.now() - t) / 100) * 100} ms`);
console.log(user2.name, orders2);
Output
One after another: ~400 ms
In parallel: ~200 ms
Asha [ 101, 102 ]
  • Promise.all rejects as soon as one promise fails.
  • Promise.allSettled waits for all and reports each result, success or failure.
  • Promise.race / Promise.any resolve with the first to finish or succeed.

Errors in async code#

Uncaught promise rejections crash the process in current Node versions. Always handle them with try/catch around await, or .catch():

JavaScript
async function loadPrices() {
  throw new Error("price service timed out");
}

try {
  await loadPrices();
} catch (err) {
  console.log("Handled:", err.message);
}
console.log("Still running");
Output
Handled: price service timed out
Still running

Common mistakes#

Practice#

Practice exercise

Predict the output order, then run it to check:

JavaScript
console.log("A");
setTimeout(() => console.log("B"), 0);
queueMicrotask(() => console.log("C"));
(async () => {
  console.log("D");
  await null;
  console.log("E");
})();
console.log("F");
Show hint

The code inside an async function runs synchronously until the first await. queueMicrotask and the continuation after await are both microtasks, queued in order.

Show solution
JavaScript
console.log("A");
setTimeout(() => console.log("B"), 0);
queueMicrotask(() => console.log("C"));
(async () => {
  console.log("D");
  await null;
  console.log("E");
})();
console.log("F");
Output
A
D
F
C
E
B

Synchronous code first (A, D, F), then the microtasks in the order they were queued (C, then E), and finally the timer (B).

Key takeaway#

Next: Building an HTTP Server.

Finished this lesson?