All interview guides

Node.js Interview Questions and Answers

24 questions that come up in Node.js technical interviews, each with the answer and an explanation of why it is right.

Topics covered: event loop, error handling, modules, streams, filesystem, async, runtime, events, memory, express, paths, performance, objects, security, http.

Test yourself — 90 question bank

1. What does this print?

Advanced
js
const fs = require('fs')
fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
})

Answer: immediate then timeout

Inside an I/O callback the loop is already in the poll phase, and check (setImmediate) comes immediately after poll, whereas timers only run on the next turn. So within I/O the order is deterministic — immediate first. At the top level it genuinely is non-deterministic, which is the distinction being tested.

Official documentation →

2. What does this print?

Intermediate
js
async function run() {
  try {
    setTimeout(() => { throw new Error('boom') }, 0)
  } catch (err) {
    console.log('caught')
  }
}
run()
console.log('after')

Answer: 'after', then the process crashes on an uncaught exception

The try/catch has already exited by the time the timer fires, so the throw happens in a fresh call stack with no handler above it. Callback errors must be handled inside the callback — try/catch cannot span an asynchronous boundary.

Official documentation →

3. What does this print?

Beginner
js
console.log(typeof module.exports)
module.exports = 42
console.log(typeof module.exports)

Answer: object then number

`module.exports` starts life as an empty object, so `typeof` is "object". Reassigning it to 42 replaces the whole export value, so the second log is "number". This is why reassigning `module.exports` after other code has already required your module does not work as expected.

Official documentation →

4. This copies a large file but uses far more memory than expected. Which line is the problem?

Intermediate
js
1  const fs = require('fs')
2  const data = fs.readFileSync('huge.bin')
3  fs.writeFileSync('copy.bin', data)
4  console.log('copied')

Answer: Line 2 — the entire file is buffered into memory at once

`readFileSync` holds the whole file in a Buffer, so peak memory tracks file size and the thread is blocked throughout. Piping streams — `createReadStream().pipe(createWriteStream())` — moves fixed-size chunks in roughly constant memory without blocking.

Official documentation →

5. This starves the event loop and the server stops responding. Which line is responsible?

Advanced
js
1  function drain(queue) {
2    if (queue.length === 0) return
3    process.nextTick(() => {
4      handle(queue.shift())
5      drain(queue)
6    })
7  }

Answer: Line 3 — nextTick recursion drains before the loop can proceed to any phase

The nextTick queue is drained completely between phases, so scheduling a new tick from inside a tick means the loop never advances to poll — no I/O, no timers, nothing. `setImmediate` yields to the loop between iterations and is the correct choice for this pattern. (shift() being O(n) is a real but secondary problem.)

Official documentation →

6. Fill in the blank to read a file asynchronously with promises.

Beginner
js
const fs = require('fs').promises
const text = ____ fs.readFile('data.txt', 'utf8')

Answer: await

`fs.promises.readFile` returns a Promise, so `await` unwraps it to the file contents. Without the `utf8` encoding argument you would get a Buffer instead of a string.

Official documentation →

7. What does this print?

Intermediate
js
const results = []
;[1, 2, 3].forEach(async (n) => {
  results.push(await Promise.resolve(n))
})
console.log(results.length)

Answer: 0

`forEach` ignores the promises its async callback returns, so it finishes immediately and the log runs before any await resolves. Use `for...of` with await, or `await Promise.all(arr.map(...))` when the work can run concurrently.

Official documentation →

8. A file is run as `node app.js one two`. What does this print?

Beginner
js
console.log(process.argv.length)

Answer: 4

`process.argv[0]` is the node executable path and `argv[1]` is the script path, then the two user arguments follow — four entries total. This is why user arguments are usually read with `process.argv.slice(2)`.

Official documentation →

9. What does this print?

Intermediate
js
const EventEmitter = require('events')
const bus = new EventEmitter()
bus.on('error', () => console.log('handled'))
bus.emit('error', new Error('x'))
console.log('still running')

Answer: 'handled' then 'still running'

`error` is a special event: with no listener registered, an EventEmitter throws and typically crashes the process. Here a listener exists, so it is called synchronously and execution continues. Always attach an error listener to emitters and streams.

Official documentation →

10. What does this print?

Advanced
js
let obj = { big: new Array(1000).fill('x') }
const wm = new WeakMap()
wm.set(obj, 'meta')
console.log(wm.has(obj))
obj = null
console.log(wm.has(obj))

Answer: true false

The second call passes `null`, not the original object, so `has` returns false regardless of GC. The real point of a WeakMap is that its key reference does not prevent collection — once nothing else holds the object, both it and its entry become collectable, which makes WeakMap the right choice for per-object metadata caches.

Official documentation →

11. Fill in the blank to propagate errors correctly when piping streams.

Intermediate
js
const { ____ } = require('stream/promises')
await ____(readable, transform, writable)

Answer: pipeline

`pipeline` wires streams together and, critically, destroys all of them if any one errors. Chained `.pipe()` calls do not — an error in the middle leaves the earlier streams open, which leaks file descriptors and memory.

Official documentation →

12. What does this print?

Beginner
js
console.log('start')
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
process.nextTick(() => console.log('nextTick'))
console.log('end')

Answer: start, end, nextTick, then timeout/immediate

Synchronous logs come first (start, end). `process.nextTick` runs before any other queued callback — it drains before the event loop continues. `setTimeout(0)` and `setImmediate` then follow, and their relative order is genuinely not guaranteed at the top level.

Official documentation →

13. What does this print?

Intermediate
js
function work() {
  return new Promise((resolve, reject) => {
    reject(new Error('failed'))
  })
}
work().catch(e => console.log('A:', e.message))
work().then(() => {}).catch(e => console.log('B:', e.message))

Answer: A: failed then B: failed

Both rejections are handled. A `.then` with no rejection handler simply passes the rejection through to the next `.catch` in the chain, so B is caught too. An unhandled rejection would only occur if no catch existed anywhere in a chain.

Official documentation →

14. This transform stream silently truncates output on large inputs. Which line is wrong?

Advanced
js
1  const { Transform } = require('stream')
2  new Transform({
3    transform(chunk, enc, cb) {
4      this.push(chunk.toString().toUpperCase())
5    }
6  })

Answer: Line 4 — cb() is never called, so the stream stalls after the first chunk

The callback signals that the chunk has been processed and the stream may supply the next one. Without it the transform accepts one chunk and then waits forever, so anything beyond the first buffer is lost. Call `cb()` after pushing, or `cb(err)` to propagate a failure.

Official documentation →

15. This should print the file contents, but prints undefined. Which line is wrong?

Beginner
js
1  const fs = require('fs')
2  const data = fs.readFile('a.txt', 'utf8', (err, contents) => {
3    return contents
4  })
5  console.log(data)

Answer: Line 2 — readFile is callback-based and returns undefined

Callback-style `fs.readFile` returns undefined immediately; the contents only arrive later, inside the callback. Returning from a callback does not send a value back to the caller. Either log inside the callback, or use `fs.promises.readFile` with await.

Official documentation →

16. This Express route hangs when the database call rejects. Which line causes it?

Intermediate
js
1  app.get('/users', async (req, res) => {
2    const users = await db.query('SELECT * FROM users')
3    res.json(users)
4  })

Answer: Line 1 — Express 4 does not forward rejections from async handlers

In Express 4 a rejected promise from an async handler is not passed to `next`, so no response is ever sent and the request hangs until it times out. Wrap handlers in a catch helper (`.catch(next)`), or use Express 5, which forwards rejections automatically.

Official documentation →

17. What does this print?

Advanced
js
async function f() {
  console.log('1')
  await null
  console.log('2')
}
f()
console.log('3')

Answer: 1 3 2

An async function runs synchronously until its first await. `await null` still yields — the value is wrapped and the remainder is queued as a microtask — so '3' runs before '2'. Awaiting a non-promise does not skip the suspension, which is a subtle source of ordering bugs.

Official documentation →

18. What does this print on a POSIX system?

Beginner
js
const path = require('path')
console.log(path.join('/users', 'ahmed', '..', 'sara', 'file.txt'))

Answer: /users/sara/file.txt

`path.join` concatenates the segments and then normalises the result, so `..` cancels the preceding `ahmed` segment. Using `path.join` instead of string concatenation is what makes path code work on both POSIX and Windows.

Official documentation →

19. A request handler builds a lookup by calling `array.find()` inside a loop over another array. Both arrays have n items. What is the complexity?

Intermediate
js
for (const order of orders) {
  const user = users.find(u => u.id === order.userId)
}

Answer: O(n²)

`find` scans linearly, and it runs once per order — n × n. Building a `Map` of users by id first costs O(n) once and turns each lookup into O(1), making the whole thing linear. This is one of the most common causes of a slow endpoint that looks fine in review.

Official documentation →

20. What does this print?

Intermediate
js
const obj = { name: 'a' }
const copy = { ...obj, nested: { x: 1 } }
const clone = { ...copy }
clone.nested.x = 99
console.log(copy.nested.x)

Answer: 99

Spread is a shallow copy: `clone.nested` and `copy.nested` are the same object reference, so mutating through one is visible through the other. For a deep copy use `structuredClone(copy)`, available in Node 17+.

Official documentation →

21. Fill in the blank to compare two secrets without leaking information through timing.

Advanced
js
const crypto = require('crypto')
const ok = crypto.____(
  Buffer.from(given), Buffer.from(expected)
)

Answer: timingSafeEqual

`timingSafeEqual` always takes the same time regardless of where the buffers first differ, so an attacker cannot narrow a token byte by byte by measuring response times. It throws if the two buffers differ in length — hash both inputs first when lengths may vary.

Official documentation →

22. Adding `await` inside a loop is always harmless because Node is asynchronous.

Intermediate
js
for (const id of ids) {
  await fetchUser(id)
}

Answer: False

This runs strictly one at a time, so 100 requests taking 50 ms each take 5 seconds instead of ~50 ms. It does not block the event loop, but it does serialise the work. Use `Promise.all` when the iterations are independent — and keep the loop when they must be sequential or you need to limit concurrency.

Official documentation →

23. What status code does this server return for any request?

Beginner
js
const http = require('http')
http.createServer((req, res) => {
  res.writeHead(201, { 'Content-Type': 'text/plain' })
  res.end('created')
}).listen(3000)

Answer: 201

`res.writeHead(201, ...)` sets the status explicitly, so every response is 201 Created. Node defaults to 200 only when you never call `writeHead` or set `res.statusCode`.

Official documentation →

24. What does this print?

Advanced
js
const { promisify } = require('util')
const sleep = promisify(setTimeout)
console.time('t')
Promise.all([sleep(100), sleep(100), sleep(100)])
  .then(() => console.timeEnd('t'))

Answer: Roughly 100 ms

All three timers are created at effectively the same moment and run concurrently, so the total is the longest one, not the sum. `Promise.all` waits for the slowest member — turning three sequential awaits into one concurrent batch is the single most common latency win in Node code.

Official documentation →

Ready to test yourself?

The full Node.js bank has 90 questions across 3 difficulty levels — timed, shuffled, and scored.

Take the Node.js quiz