Skip to content
Node.js interview 5 min read

Interview Questions: Event Loop & Async

The event loop is the single most common source of “gotcha” interview questions for Node.js, because it touches everything: timers, I/O, Promises, and the illusion of concurrency on a single thread. Interviewers use ordering puzzles to test whether you actually understand the runtime or merely memorized that “Node is asynchronous.” This page collects the questions that come up most, with precise answers and runnable examples using modern Node.js (20/22 LTS) and ES modules.

How does the event loop work if Node is single-threaded?

Your JavaScript runs on one thread with a single call stack, so two lines of your code never execute literally at the same time. Concurrency comes from libuv, the C library underneath Node, which hands blocking work (file reads, network sockets, DNS) to the OS kernel and a small worker thread pool. When that work completes, libuv queues a callback, and the event loop pushes it back onto the JS thread when the stack is empty.

The loop runs in a fixed sequence of phases, each with its own FIFO queue:

PhaseProcesses
timerssetTimeout / setInterval callbacks whose threshold elapsed
pending callbacksDeferred system callbacks (e.g. some TCP errors)
pollRetrieves and runs I/O callbacks; may block here waiting for work
checksetImmediate callbacks
closeclose event handlers, e.g. socket.on('close', ...)

Because there is one thread, a CPU-bound synchronous loop blocks everything — no timers fire, no I/O callbacks run, and no new connections are accepted until it finishes.

What is the difference between microtasks and macrotasks?

Macrotasks are the callbacks queued in the phases above — timers, I/O, setImmediate. Microtasks are not part of the phase rotation and have higher priority. There are two microtask queues: process.nextTick (Node-specific, highest priority) and the Promise job queue (.then, await, queueMicrotask).

After every macrotask completes — and after each phase transition — Node fully drains the nextTick queue first, then the entire Promise microtask queue, before running the next macrotask. This is why a resolved Promise’s .then always beats a setTimeout(fn, 0).

console.log('start');

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

console.log('end');

Output:

start
end
nextTick
promise
timeout

Synchronous code runs first, then nextTick (highest priority microtask), then Promises, and only then the timer macrotask.

process.nextTick vs setImmediate — which runs first?

Despite the names, process.nextTick does not wait for the next loop tick — it fires immediately after the current operation, before any I/O or timers. setImmediate fires in the check phase, after the poll phase. So nextTick always runs before setImmediate.

import { readFile } from 'node:fs/promises';

setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));

await readFile(import.meta.filename);
console.log('after file read');

Output:

nextTick
immediate
after file read

Overusing process.nextTick can starve the event loop: because it drains fully before I/O, a recursive nextTick chain blocks all other phases. Prefer setImmediate when you just want to yield.

Why is setTimeout vs setImmediate ordering non-deterministic?

At the top level, setTimeout(fn, 0) and setImmediate(fn) race: the result depends on how long the process took to start and reach the timers phase. The 0ms timeout might not be “ready” yet when the loop first checks. But inside an I/O callback, the order is guaranteed — the loop is already past timers and enters the check phase next, so setImmediate always wins.

import { readFile } from 'node:fs/promises';

await readFile(import.meta.filename);
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

Output:

immediate
timeout

Callbacks vs Promises vs async/await — what should I say?

All three describe asynchronous control flow, but they differ in ergonomics and error handling. Callbacks are the original Node convention ((err, result) => {}) but lead to nesting and easy-to-miss error checks. Promises flatten the nesting and compose. async/await is syntactic sugar over Promises that lets asynchronous code read sequentially while still being non-blocking.

ApproachError handlingCompositionNotes
CallbacksManual if (err) per callHard (callback hell)Still used by some core APIs
Promises.catch()Promise.all, chainingFoundation for await
async/awaittry/catchSequential reads cleanlyBuilt on Promises; non-blocking
// async/await with a try/catch
async function loadUser(id) {
  try {
    const res = await fetch(`https://api.example.com/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('Failed to load user:', err.message);
    throw err;
  }
}

Key point for interviews: await does not block the thread — it suspends the async function and returns control to the event loop, resuming via the Promise microtask queue when the awaited value settles.

How do I run async work concurrently?

A common follow-up. Sequential await in a loop is slow; use Promise.all to run independent operations concurrently and Promise.allSettled when you want every result regardless of failures.

const ids = [1, 2, 3];

// Concurrent — all requests start immediately
const users = await Promise.all(ids.map((id) => loadUser(id)));

// Tolerate partial failures
const results = await Promise.allSettled(ids.map((id) => loadUser(id)));
const ok = results.filter((r) => r.status === 'fulfilled');

Best Practices

  • Keep per-tick work small; offload CPU-bound tasks to Worker Threads or a child process so the loop stays responsive.
  • Reach for setImmediate over process.nextTick to yield without starving I/O.
  • Always attach a .catch() or wrap await in try/catch — an unhandled rejection can crash the process in modern Node.
  • Use Promise.all for independent async work instead of awaiting in a loop sequentially.
  • Remember the priority order: synchronous code → process.nextTick → Promise microtasks → macrotask phases.
  • Don’t rely on top-level setTimeout vs setImmediate ordering; it is only deterministic inside an I/O callback.
Last updated June 14, 2026
Was this helpful?