Menu
CoddyTech

Node.js Event Loop and Async Interview Questions

How Node.js runs timers, I/O callbacks and promises on one thread, and the ordering questions built on that.

11 questions
  1. Explain the Node.js event loop and its phases.

    The event loop is libuv's loop that runs ready callbacks on the main thread. Each turn has six phases, each with its own queue:

    1. timers: due setTimeout and setInterval callbacks
    2. pending callbacks: some deferred system I/O callbacks
    3. idle, prepare: internal
    4. poll: wait for and run I/O callbacks
    5. check: setImmediate callbacks
    6. close callbacks: 'close' events

    After every callback (timers and setImmediate too, since Node 11), Node drains process.nextTick, then promise microtasks. See the event loop.

  2. In what order do setTimeout(0), queueMicrotask and Promise.then run?

    JavaScript
    const log = [];
    setTimeout(() => log.push('timeout'), 0);
    queueMicrotask(() => log.push('microtask'));
    Promise.resolve().then(() => log.push('promise'));
    log.push('sync');
    setTimeout(() => console.log(log.join(' ')), 5);

    Sync code runs first, then microtasks in queueing order: queueMicrotask was called before .then. The loop reaches the timers phase only when the microtask queue is empty, even for a 0 ms timer.

  3. What is the difference between process.nextTick, setImmediate and setTimeout(fn, 0)?

    process.nextTick runs right after the current operation, before promise callbacks (in CommonJS; see the ES module question). setImmediate runs in the check phase, after I/O polling. setTimeout(fn, 0) runs in the timers phase after at least 1 ms.

    // Node.js, CommonJS file
    setTimeout(() => console.log('timeout'), 0);
    setImmediate(() => console.log('immediate'));
    Promise.resolve().then(() => console.log('promise'));
    process.nextTick(() => console.log('nextTick'));
    console.log('sync');
    

    Node prints sync, nextTick, promise, then timeout and immediate in either order. The names are backwards: nextTick is the immediate one, and a recursive nextTick can starve I/O.

  4. Does setTimeout(fn, 0) or setImmediate run first?

    From the main module it is not guaranteed: it depends on whether 1 ms has passed when the loop reaches the timers phase. Inside an I/O callback setImmediate always runs first, because check comes right after poll.

    const fs = require('node:fs');
    
    fs.readFile(__filename, () => {
      setTimeout(() => console.log('timeout'), 0);
      setImmediate(() => console.log('immediate'));
    });
    // Node.js always prints:
    // immediate
    // timeout
    

    setImmediate is a Node.js API, not a web standard, so this order is a Node.js rule; see setImmediate vs setTimeout.

  5. Why does process.nextTick run after a promise callback in an ES module but before it in CommonJS?

    Because an ES module's top-level code runs inside a promise job. When a CommonJS file ends, Node drains nextTick first, then microtasks. An ES module body ends while microtasks are already draining, so its promise callbacks run first.

    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('sync');
    
    FileNode.js prints
    order.cjssync, nextTick, promise
    order.mjssync, promise, nextTick

    The process.nextTick docs show both orders. Never depend on this order; make it explicit with await.

  6. What happens when you block the event loop?

    Everything waits: timers fire late, requests queue up, health checks time out.

    JavaScript
    const start = Date.now();
    
    setTimeout(() => {
      const waited = Date.now() - start;
      console.log('asked for 10 ms, waited at least 200:', waited >= 200);
    }, 10);
    
    // 200 ms of synchronous work, like a huge JSON.parse or a sync crypto call
    while (Date.now() - start < 200) {}
    console.log('busy loop done');

    Common causes: JSON.parse on huge payloads, sync fs or crypto in a handler, a regex with catastrophic backtracking. Measure with perf_hooks.monitorEventLoopDelay(); fix by chunking with setImmediate or a worker thread.

  7. What is the libuv thread pool, and which operations use it?

    A pool of threads (4 by default) that libuv uses for work the OS cannot do asynchronously. Your JavaScript never runs there.

    Uses the poolDoes not
    fs (not fs.watch)sockets, HTTP
    dns.lookupdns.resolve*
    async crypto.pbkdf2, scrypttimers
    async zlibchild process pipes

    The trap: with 4 threads, 8 concurrent pbkdf2 calls take about twice as long as 4. Raise it with the UV_THREADPOOL_SIZE environment variable before Node starts (maximum 1024).

  8. How can one Node.js thread handle thousands of concurrent connections?

    A waiting connection costs almost nothing: Node registers each socket with the OS (epoll, kqueue, IOCP) and runs code only when data arrives, where a thread-per-request server parks a thread per client. The limit is CPU per request: at 5 ms of JavaScript each, one process tops out near 200 requests per second.

  9. Does forEach wait for an async callback?

    JavaScript
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function run() {
      const done = [];
      [30, 10, 20].forEach(async (ms) => {
        await sleep(ms);
        done.push(ms);
      });
      const afterLoop = done.length;
      await sleep(50);
      console.log(afterLoop, done.join(','));
    }
    run();

    forEach ignores the promises its callback returns, so right after the loop nothing is done, and the sleeps finish in time order. Use await Promise.all(items.map(...)) (parallel) or for...of with await (one at a time). See async and await.

  10. How do you turn a callback API into a promise in Node.js?

    Use util.promisify for any function with an error-first callback, or the promise APIs Node ships: fs/promises, timers/promises, stream/promises.

    JavaScript
    const util = require('node:util');
    
    function legacyDouble(x, callback) {
      setTimeout(() => {
        if (x < 0) return callback(new Error('negative input'));
        callback(null, x * 2);
      }, 1);
    }
    
    const double = util.promisify(legacyDouble);
    
    async function main() {
      console.log(await double(21));
      try {
        await double(-1);
      } catch (err) {
        console.log('caught:', err.message);
      }
    }
    main();
  11. What is the difference between Promise.all, Promise.allSettled, Promise.race and Promise.any?

    They differ in when they settle:

    MethodFulfillsRejects
    allall fulfill, with an arrayat the first rejection
    allSettledall settle, with a status eachnever
    racefirst to settle, if it fulfilledfirst to settle, if it rejected
    anyfirst to fulfillall reject (AggregateError)
    JavaScript
    const ok = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    const fail = (ms, msg) => new Promise((_, reject) => setTimeout(() => reject(new Error(msg)), ms));
    
    async function main() {
      try {
        await Promise.all([ok(20, 'a'), fail(10, 'b failed'), ok(30, 'c')]);
      } catch (err) {
        console.log('all:', err.message);
      }
      const settled = await Promise.allSettled([ok(20, 'a'), fail(10, 'b failed')]);
      console.log('allSettled:', settled.map((r) => r.status).join(', '));
      console.log('race:', await Promise.race([ok(20, 'slow'), ok(5, 'fast')]));
      console.log('any:', await Promise.any([fail(5, 'x'), ok(20, 'first success')]));
    }
    main();

    When Promise.all rejects, the other operations keep running; nothing is cancelled.

Coddy programming languages illustration

Learn to code with Coddy

Get started