Node.js Interview Questions and Answers: Event Loop, Async and More

Node.js interview questions banner showing the event loop connected to async/await, streams, workers and modern Node
Node.js Interview Questions Banner showing the Node.js event loop connected to topics: async code, streams, worker threads and modern Node features. DEVDOJO · CAREER PREP Node.js Interview Questions Event loop, async code, streams and scaling — explained simply, with runnable answers Event Loop async/await Streams Workers Modern Node
Quick Summary
  • This guide covers the Node.js interview questions that come up most often for junior and mid-level backend roles, with short answers you can say out loud.
  • The big topics are the event loop, async code (callbacks, promises, async/await), streams, and scaling (cluster vs worker threads).
  • Every answer comes with a small, runnable code example. Run them with Node.js 24 or 26.
  • Interviewers now also ask about modern built-in features: fetch, the test runner, --watch, --env-file and running TypeScript directly.

If you are preparing for a backend or full-stack job, you will almost certainly face Node.js interview questions. The good news: interviewers keep coming back to the same core ideas. If you truly understand the event loop, async code, streams and how Node.js scales, you can answer most questions with confidence — even the tricky “what is the output?” puzzles.

In this guide we go through the questions one by one. Each answer has a simple explanation first, then a small code example you can run on your own laptop. No memorising — the goal is to understand.

How to Use This Guide

Questions are tagged by level so you can focus on what matters for your role:

  • Beginner — expected for freshers and interns.
  • Intermediate — expected with 1–3 years of experience.
  • Advanced — senior-leaning questions that show deeper understanding.
Which Node.js version? Use an LTS (Long-Term Support) version. Node.js 24 is the current Active LTS, and Node.js 26 is scheduled to become LTS on 28 October. All examples here work on both. You can check the official dates on the Node.js release schedule.

Node.js Basics Questions

1. What is Node.js? Beginner

Node.js is a runtime that lets you run JavaScript outside the browser — on servers, laptops and build tools. It uses Google’s V8 engine to run your code and a C library called libuv to handle files, network and timers without blocking.

Say this in the interview: “Node.js is a JavaScript runtime built on V8. It uses an event-driven, non-blocking I/O model, which makes it great for APIs, real-time apps and tools that handle many connections at once.”

2. Is Node.js single-threaded? Intermediate

Your JavaScript runs on one main thread. But Node.js itself is not fully single-threaded. libuv keeps a small thread pool (4 threads by default) for slow work like file system calls, DNS lookups, compression and some crypto. Network sockets use the operating system’s own async features.

So the correct answer is: “JavaScript execution is single-threaded, but Node.js uses extra threads in the background for I/O. And we can create more threads ourselves with worker_threads.”

3. What is the difference between blocking and non-blocking code? Beginner

Blocking code makes the main thread wait until the task finishes. While it waits, Node.js cannot serve any other request. Non-blocking code starts the task, moves on, and runs a callback (or resolves a promise) when the result is ready.

blocking.mjs

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

// Blocking: nothing else can run until the file is fully read
const a = readFileSync('./package.json', 'utf8');
console.log('sync read done:', a.length, 'chars');

// Non-blocking: the main thread is free while the OS reads the file
const pending = readFile('./package.json', 'utf8');
console.log('this line runs before the async read finishes');
const b = await pending;
console.log('async read done:', b.length, 'chars');
Rule of thumb: *Sync functions are fine in startup scripts and CLI tools. Avoid them inside request handlers of a web server.

4. What is the difference between CommonJS and ES Modules? Beginner

CommonJS (CJS) is Node’s original module system: require() and module.exports. ES Modules (ESM) is the JavaScript standard: import and export. ESM supports top-level await and is what new projects should use.

Node.js treats a file as ESM if it ends in .mjs, or ends in .js and the nearest package.json has "type": "module". A bonus point for interviews: modern Node versions (22.12+ and later) can require() an ES module directly, as long as that module does not use top-level await. This removed a big pain point when mixing old and new packages.

5. What is npm, and what is package-lock.json for? Beginner

npm is the package manager that ships with Node.js. package.json lists the version ranges you accept (like ^5.1.0). package-lock.json records the exact versions that were installed, so every developer and every server gets the same dependency tree. In CI, use npm ci, which installs strictly from the lock file. (We used exactly this in our GitHub Actions CI/CD for Node.js guide.)

Event Loop Questions

The event loop is the number one topic in Node.js interviews. If you can draw it on a whiteboard, you are already ahead of most candidates.

6. What is the event loop? Intermediate

The event loop is the mechanism that lets one JavaScript thread handle thousands of operations. Node.js hands slow work (files, network, timers) to the system, and when that work finishes, a callback is placed in a queue. The event loop keeps checking these queues and runs the callbacks one by one, in a fixed order of phases.

Diagram of the Node.js event loop phases Five phases in a loop: timers, pending callbacks, poll, check and close callbacks, with the microtask queue for nextTick and promises running between each step. 1. TimerssetTimeout, setInterval 2. Pendingsome system errors 3. PollI/O callbacks (files, net) 4. ChecksetImmediate 5. Closesocket.on(‘close’) Loop again?if work is pending Microtask queue 1st: process.nextTick() 2nd: Promise callbacks
The event loop runs phases in order. After each callback, Node.js empties the microtask queue (nextTick first, then promises).

Explore the phases (click a tab)

Timers phase. Runs callbacks from setTimeout and setInterval whose time has passed. Note: setTimeout(fn, 0) means “at least 0 ms later”, not “right now”. Delays are a minimum, never a guarantee.
Pending callbacks phase. Runs a few system-level callbacks that were deferred from the previous loop, such as certain TCP errors. You rarely touch this directly, but naming it shows you know the full picture.
Poll phase. The busiest phase. Node.js collects finished I/O events (file reads, incoming HTTP data) and runs their callbacks. If there is nothing to do, the loop can wait here for new events.
Check phase. Runs setImmediate callbacks right after the poll phase. That is why, inside an I/O callback, setImmediate always runs before setTimeout(fn, 0).
Close callbacks phase. Runs cleanup callbacks such as socket.on('close', ...) when a handle is closed abruptly.
Microtasks (between everything). Not a phase, but very important. After every single callback, Node.js runs all process.nextTick callbacks, then all promise callbacks (.then, code after await). Too many microtasks can starve the event loop.

7. What is the output of this code? Advanced

This “guess the order” puzzle is very common. Try to answer before opening the solution.

order.cjs

const fs = require('node:fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('setTimeout'), 0);
  setImmediate(() => console.log('setImmediate'));
  Promise.resolve().then(() => console.log('promise'));
  process.nextTick(() => console.log('nextTick'));
  console.log('inside I/O callback');
});

console.log('main script');
Show the answer and explanation
main script
inside I/O callback
nextTick
promise
setImmediate
setTimeout

Why: synchronous code runs first. The file read finishes later in the poll phase. Inside that callback, the sync console.log runs, then the microtask queue empties (nextTick before promise). Next comes the check phase (setImmediate), and only on the next loop the timers phase (setTimeout).

Trap: if you put setTimeout and setImmediate in the main script (not inside an I/O callback), their order is not guaranteed. Saying this out loud impresses interviewers. Also note we used a .cjs file on purpose: in ES modules, the promise can run before nextTick, because the module itself is already running inside a microtask.

8. What is the difference between process.nextTick() and setImmediate()? Intermediate

The names are confusing — they are almost swapped! process.nextTick() runs immediately after the current operation, before the event loop continues. setImmediate() runs in the check phase of the next loop iteration. Prefer setImmediate() or queueMicrotask() in normal code; a recursive nextTick can block I/O forever.

Async Code Questions

9. Callbacks vs Promises vs async/await — what is the difference? Beginner

They are three styles of handling a result that arrives later. Callbacks pass a function to be called later (error first: (err, data)). Nesting many callbacks leads to “callback hell”. Promises represent a future value and can be chained with .then(). async/await is cleaner syntax on top of promises that reads like normal top-to-bottom code. For a deeper refresher, read our guide to JavaScript Promises.

styles.mjs

import fs from 'node:fs';
import { promisify } from 'node:util';

// 1) Callback style (error-first)
fs.readFile('./package.json', 'utf8', (err, data) => {
  if (err) return console.error('callback error:', err.message);
  console.log('callback:', JSON.parse(data).name);
});

// 2) Promise style
const readFileP = promisify(fs.readFile);
readFileP('./package.json', 'utf8')
  .then((data) => console.log('promise:', JSON.parse(data).name))
  .catch((err) => console.error('promise error:', err.message));

// 3) async/await style (most readable)
try {
  const data = await fs.promises.readFile('./package.json', 'utf8');
  console.log('await:', JSON.parse(data).name);
} catch (err) {
  console.error('await error:', err.message);
}

10. Promise.all vs Promise.allSettled vs Promise.race vs Promise.any? Intermediate

  • Promise.all — waits for all; fails fast if any one rejects.
  • Promise.allSettled — waits for all and gives you every result, success or failure.
  • Promise.race — settles with the first promise to finish (success or failure).
  • Promise.any — resolves with the first success; rejects only if all fail.

Here is a real-world example: call several APIs in parallel, with a timeout, and report every result. Node.js has a built-in fetch, so no extra package is needed.

parallel.mjs

const urls = [
  'https://api.github.com/users/nodejs',
  'https://api.github.com/users/this-user-should-not-exist-404',
  'https://example.invalid/',
];

const results = await Promise.allSettled(
  urls.map(async (url) => {
    // Cancel the request if it takes longer than 3 seconds
    const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
    if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
    return res.json();
  })
);

for (const r of results) {
  console.log(r.status === 'fulfilled' ? `OK: ${r.value.login}` : `FAILED: ${r.reason.message}`);
}
Interview tip: mention that a common bug is using await inside a for loop for independent calls. That runs them one after another. Use Promise.all to run them in parallel.

11. How do you handle errors in async code? Intermediate

Use try/catch around await, or .catch() on promise chains. Never leave a promise without error handling: since Node.js 15, an unhandled promise rejection crashes the process by default. As a last line of defence, log and exit cleanly:

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1); // let your process manager (Docker, PM2, systemd) restart the app
});

12. What is an EventEmitter? Beginner

EventEmitter is the class behind most Node.js APIs (HTTP servers, streams, sockets). You emit named events and other code listens with on. It is the “publish / subscribe” pattern.

orders.mjs

import { EventEmitter } from 'node:events';

class OrderService extends EventEmitter {
  place(order) {
    // ...save the order to the database...
    this.emit('order:placed', order);
  }
}

const orders = new OrderService();
orders.on('order:placed', (o) => console.log(`Send email for order #${o.id}`));
orders.on('order:placed', (o) => console.log(`Update stock for ${o.item}`));

orders.place({ id: 101, item: 'Mechanical keyboard' });
Watch out: if you emit an 'error' event and nobody listens for it, Node.js throws and can crash the process. Always add an error listener.

Streams and Buffers Questions

13. What are streams, and why use them? Intermediate

A stream processes data piece by piece (in chunks) instead of loading everything into memory. Reading a 5 GB log file with readFile can crash your server; with a stream, memory stays small and steady. There are four types: Readable, Writable, Duplex (both, like a TCP socket) and Transform (changes data as it passes, like gzip).

The modern way to connect streams is pipeline from node:stream/promises. It handles errors and cleanup for you — something plain .pipe() does not do.

compress.mjs

import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { pipeline } from 'node:stream/promises';

// Usage: node compress.mjs big-file.log
const input = process.argv[2] ?? 'big-file.log';

try {
  await pipeline(
    createReadStream(input),          // Readable
    createGzip(),                     // Transform
    createWriteStream(`${input}.gz`)  // Writable
  );
  console.log(`Compressed ${input} -> ${input}.gz`);
} catch (err) {
  console.error('Pipeline failed:', err.message);
}

14. What is backpressure? Advanced

Backpressure happens when data arrives faster than the destination can handle it — for example, reading a fast disk and writing to a slow network. If you ignore it, data piles up in memory. Streams solve this automatically: when the writable’s buffer is full, write() returns false and the readable pauses until a 'drain' event. Using pipeline() gives you this behaviour for free.

15. What is a Buffer? Beginner

A Buffer is a fixed-size chunk of raw binary data (bytes). Node.js uses Buffers for files, network packets and images. Buffer.from('hello') creates one from a string, and buf.toString('base64') converts it. Buffers are a subclass of Uint8Array, so they work with standard web APIs too.

Scaling and Performance Questions

16. Worker threads vs cluster vs child process — when to use which? Advanced

All three let you use more than one CPU core, but for different jobs:

Comparison of cluster, worker threads and child process in Node.js Three columns. Cluster: many Node processes sharing one port, best for scaling web servers. Worker threads: threads inside one process with shared memory, best for CPU-heavy tasks. Child process: runs a separate program like ffmpeg or a Python script. cluster many processes, one port Primary :3000 Node Node Node Best for: scaling an HTTP server across all CPU cores worker_threads threads in one process main worker worker Best for: CPU-heavy work: hashing, image resizing, big JSON child_process run another program Node app ffmpeg / python / git Best for: calling shell commands or non-JS tools
Pick the tool by the job: cluster for more web traffic, worker threads for heavy calculations, child process for other programs.

Here is a worker thread doing a slow calculation, so the main thread stays free to answer HTTP requests:

fib-worker.mjs

import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';

function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2); // deliberately slow (CPU-heavy)
}

if (isMainThread) {
  // Main thread: start a worker running this same file
  const worker = new Worker(new URL(import.meta.url), { workerData: 40 });
  worker.on('message', (result) => console.log('fib(40) =', result));
  worker.on('error', (err) => console.error('Worker failed:', err));
  console.log('Main thread is still free while the worker calculates...');
} else {
  // Worker thread: do the heavy work and send the result back
  parentPort.postMessage(fib(workerData));
}

In production, most teams don’t use cluster by hand. They run several containers behind a load balancer (see our Docker Compose tutorial) or use a process manager like PM2.

17. How do you find a memory leak or slow code? Advanced

Start with process.memoryUsage() to watch heap growth over time. Then run node --inspect app.js and open chrome://inspect to take heap snapshots and CPU profiles. Common leak causes: global arrays or caches that never shrink, event listeners added on every request and never removed, and timers that are never cleared.

Modern Node.js Features Questions

Interviewers increasingly ask, “Which new Node.js features do you use?” It checks whether you keep up. These are all built in — no extra packages:

18. What tools are now built into Node.js? Intermediate

  • fetch — the same API as the browser, for HTTP requests.
  • node --watch app.js — restarts on file changes (replaces nodemon for many projects).
  • node --env-file=.env app.js — loads environment variables without the dotenv package.
  • node:test — a built-in test runner with describe, it and mocking.
  • Running TypeScript directly — recent versions strip type annotations, so node app.ts works for code that only uses “erasable” TypeScript syntax (no enum or namespace). Read the official Node.js TypeScript docs for the details, and see our TypeScript 7 migration guide for type checking.

Here is a complete test using only the built-in runner. Run it with node --test:

math.test.mjs

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

function applyDiscount(price, percent) {
  if (percent < 0 || percent > 100) throw new RangeError('percent must be 0-100');
  return Math.round(price * (100 - percent)) / 100;
}

describe('applyDiscount', () => {
  it('applies a 10% discount', () => {
    assert.equal(applyDiscount(500, 10), 450);
  });

  it('throws for an invalid percent', () => {
    assert.throws(() => applyDiscount(500, 120), RangeError);
  });
});

API and Express Questions

19. What is middleware in Express? Beginner

Middleware is a function that runs between the incoming request and your final route handler. It receives (req, res, next) and can read or change the request, end the response, or call next() to pass control on. Logging, authentication, body parsing and rate limiting are all middleware. An error-handling middleware has four arguments: (err, req, res, next).

In Express 5, if an async route throws or rejects, the error goes to your error handler automatically — no more wrapping every route in try/catch.

server.mjs

// npm install express
import express from 'express';

const app = express();
app.use(express.json());

// Simple logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`));
  next();
});

app.get('/users/:id', async (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) {
    const err = new Error('User id must be a number');
    err.status = 400;
    throw err; // Express 5 forwards this to the error handler
  }
  res.json({ id, name: 'Asha' });
});

// Error-handling middleware: 4 arguments, registered last
app.use((err, req, res, next) => {
  res.status(err.status ?? 500).json({ error: err.message });
});

app.listen(3000, () => console.log('API running on http://localhost:3000'));

20. How do you keep a Node.js API secure? Intermediate

A good short answer covers: validate all input (for example with Zod), use parameterised queries to stop SQL injection, keep secrets in environment variables (never in Git), add security headers (the helmet package), rate-limit login routes, run npm audit in CI, and keep Node.js on a supported LTS version so you get security patches.

Common Mistakes and Fixes

Interviewers love asking “What went wrong here?” These are the errors you will see most in real Node.js projects:

Error or symptomWhy it happensFix
Cannot use import statement outside a moduleThe file is treated as CommonJS.Add "type": "module" to package.json or rename the file to .mjs.
ERR_HTTP_HEADERS_SENTThe code sends a response twice, often a missing return.Use return res.json(...) so the function stops after responding.
EADDRINUSE: address already in use :::3000Another process already uses the port.Stop the old process or use process.env.PORT with a different value.
Process crashes with “unhandled rejection”A promise rejected and nothing caught it.Add try/catch around await, or .catch() on chains.
Server freezes under loadCPU-heavy work or *Sync calls block the event loop.Move heavy work to worker_threads; use async APIs.
MaxListenersExceededWarningListeners are added on every request and never removed.Add listeners once, or use once() / off().
Slow API with many awaits in a loopIndependent calls run one by one.Run them together with Promise.all.

Interview Tips and Best Practices

  • Explain, then show. Give a one-line answer, then a tiny example. Interviewers want to see that you understand why.
  • Draw the event loop. If there is a whiteboard, sketch the phases and the microtask queue. It shows clear thinking.
  • Talk about trade-offs. “Node.js is great for I/O-heavy APIs, but for CPU-heavy work I would use worker threads or a different service.”
  • Mention production habits. Logging, graceful shutdown on SIGTERM, health-check routes, and CI tests show real-world experience.
  • Build one small project. A REST API with tests, a database and a Dockerfile gives you stories to tell for almost every question.
  • Know your version. Say which Node.js LTS you use and one new feature you like. It signals that you keep learning.
Practice idea: run every code example in this post, then change one line and predict the new output before running it again. This is the fastest way to make the event loop “click”.

Frequently Asked Questions

What are the most asked Node.js interview questions for freshers?

For freshers, expect: What is Node.js? Is it single-threaded? What is the event loop? Blocking vs non-blocking code, callbacks vs promises vs async/await, CommonJS vs ES Modules, what npm and package.json do, and what Express middleware is.

Is Node.js good for CPU-heavy tasks?

Not on the main thread. Long calculations block the event loop and every other request waits. Move CPU-heavy work to worker_threads, a job queue, or a separate service written in a language better suited to it.

Which Node.js version should I learn for interviews?

Learn on the current LTS version. Node.js 24 is in Active LTS, and Node.js 26 becomes LTS at the end of October. The core concepts — event loop, streams, async code — are the same in both.

Do I need to know Express for a Node.js interview?

Usually yes. Express is still the most common web framework in job descriptions. Knowing routing, middleware and error handling is enough for most roles. Knowing a second framework like Fastify or NestJS is a bonus.

What is the difference between Node.js and JavaScript?

JavaScript is the programming language. Node.js is a runtime that runs JavaScript outside the browser and adds server APIs like the file system, HTTP and streams. Browsers add different APIs, like the DOM.

Conclusion

Most Node.js interview questions come from a few core ideas: one JavaScript thread plus an event loop, non-blocking I/O, promises and async/await, streams for big data, and the right tool for scaling. Understand those, run the examples above, and you will be able to answer both the simple questions and the tricky “what is the output?” puzzles calmly.

Bookmark this page, practise one section a day, and try explaining each answer to a friend. If you can teach it, you can answer it in an interview.

New posts every day on DevDojo

We publish practical, beginner-friendly guides on Node.js, React, Python, AI and DevOps every day. Next, set up automatic testing for your project with our GitHub Actions CI/CD guide for Node.js — and leave your toughest interview question in the comments below.

Share