Node.js interview questions on async patterns: callbacks, Promises, async/await
Reviewed by Mark Dickie · Last updated
4 questions on this page you can answer and see marked on the spot. Go to the first one
Node.js async patterns are the ways code handles work that finishes later, such as file reads and network calls. Interviewers usually check whether you can move from error-first callbacks to Promises and async/await, and whether you can predict event loop order well enough to explain a program's output. Expect follow-ups on failure handling, concurrency limits, and cancellation, not just syntax.
Which async patterns does a Node.js interview cover?
| Pattern | What to know | Common failure mode |
|---|---|---|
| Error-first callbacks | The (err, result) signature, and util.promisify to wrap them | Calling the callback twice when an error path falls through without a return |
| Promises | Promise.all, Promise.allSettled, Promise.race and when each one settles | Promise.all rejects on the first failure, so the other results are lost unless you use allSettled |
| async/await | try/catch around await; a for...of loop runs each await in turn | Serial awaits in a loop that could have run together with Promise.all |
| Event loop order | process.nextTick and microtasks run before timers and I/O callbacks | Assuming setTimeout(fn, 0) fires before setImmediate from the main module; that order is not fixed |
| Streams | pipeline() from stream/promises, and backpressure through the false return of write() | Ignoring a false return from write(), so buffers grow until memory runs out |
| Cancellation | AbortController passed to fetch and timers/promises | Aborting the signal but never checking it inside a long loop |
How should you practise Node.js async patterns before an interview?
- Rewrite a callback-based function with util.promisify, then add a timeout that rejects.
- Build a retry helper with exponential backoff and a fixed maximum attempt count.
- Run 100 fetch calls with a concurrency limit of 5 using a small worker pool.
- Predict the output of process.nextTick, a resolved Promise, setTimeout and setImmediate before you run them.
Key facts
- Tarmac has 19 Node.js interview questions on this topic, 10 of them on this page, at difficulty 1–5 of 5.
- Tarmac tracked 1,070 job postings asking for Node.js in October 2026.
- Roles asking for Node.js advertise a median base salary of £80,000, across 231 job postings as of October 2026.
- Tarmac last reviewed these Node.js interview questions on 9 November 2026.
At a glance
| Questions | 10 shown · 19 in the bank |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | Multiple answer, Fill in the blank, Multiple choice, Flashcard, Code output, Find the bug, Ordering, Short answer |
What you'll review
- async context
- error first callbacks
- promisify
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Which of the following statements about Node.js AsyncLocalStorage are correct?#
Options
Pick every one that applies.
Show answer
The correct statements are: AsyncLocalStorage propagates stored state along async execution chains without explicit parameter passing; it is a built-in API from node:async_hooks with no npm dependency; and als.run(store, callback) makes the store available only within that callback and its async descendants, not sibling or parent contexts. It does not propagate context across separate process instances.
AsyncLocalStorage is a built-in Node.js API exported from node:async_hooks (no npm install needed). It stores state that automatically follows the asynchronous execution path, so callbacks, promises, and timers spawned from a run() call can retrieve it via getStore() without explicit parameter threading. The run(store, callback) method scopes the store to callback and its async descendants only; sibling or parent contexts do not see it. It does NOT cross process boundaries; each process has its own independent storage, so the claim that it propagates context across separate Node.js process instances is incorrect.
In Node.js, the error-first callback convention requires that the first argument passed to the callback is the _____ (or null if the operation succeeded), and the second argument is the _____ data.#
Show answer
In Node.js, the error-first callback convention requires that the first argument passed to the callback is the error (or null if the operation succeeded), and the second argument is the result data.
Example:
const fs = require('fs');
fs.readFile('/path/to/file', 'utf8', (err, data) => {
if (err) {
console.error('Failed:', err);
return;
}
console.log(data);
});
The error-first callback convention (also called Node-style callbacks) mandates that the first parameter of the callback is always an error value — an Error object when something went wrong, or null when the operation succeeded. The second parameter carries the successful result data. This lets the caller check the first argument before safely using the second.
In Node.js, what does util.promisify(func) expect of the original func so that the returned promise resolves (instead of rejects) with the correct value?#
Options
Show answer
util.promisify expects the original function to follow Node's error-first callback convention: the last argument is a callback invoked as callback(err, value). The returned promise resolves with value when err is null/undefined and rejects with err otherwise. Functions using callback(value) or callback(value, err) or already returning promises do not match this contract.
util.promisify takes a function that follows the standard Node.js error-first callback pattern — the callback is the last argument and is called as callback(err, ...values). When err is null or undefined the returned promise resolves with the second argument (value); when err is truthy the promise rejects with err. The other options describe incompatible conventions that util.promisify does not support by default.
What does util.promisify do, and what shape of function does it expect?#
Show answer
util.promisify(fn) returns a version of fn that yields a Promise instead of taking a callback. It expects fn to follow the error-first callback convention — the callback is the last argument and is called as (err, value). On err the promise rejects; otherwise it resolves with value. (A function can override the behaviour via the util.promisify.custom symbol.)
util.promisify bridges the older error-first callback world to promises/async-await, as long as the callback is last and uses the (err, result) signature. Many core APIs also expose promise variants directly (e.g. fs.promises).
An error-first callback API is wrapped with util.promisify. What does this log?#
const { promisify } = require('util');
function getValue(cb) {
cb(null, 42);
}
const getValueAsync = promisify(getValue);
getValueAsync().then((v) => console.log(v));Show answer
42
util.promisify turns a function whose last argument is an error-first callback (err, value) into one that returns a Promise. Calling cb(null, 42) means no error and a result of 42, so the returned promise resolves with 42.
This uses a typical async-style API where the callback is invoked on a later tick. What does it log, in order?#
function load(cb) {
process.nextTick(() => cb(null, 'data'));
}
console.log('start');
load((err, result) => console.log(result));
console.log('end');Show answer
start
end
data
The convention for asynchronous APIs is to defer the callback to a later tick. console.log('start') and console.log('end') run synchronously first; the deferred error-first callback (no error, result 'data') runs afterwards, logging data last.
This reads a file but will crash on the happy path instead of handling failures. Which line is the bug?#
const fs = require('fs');
fs.readFile('./data.json', 'utf8', (data) => {
console.log(JSON.parse(data));
});Show answer
The bug is on line 2.
Node's error-first callbacks receive (err, data) — the first argument is the error. The callback here binds the first argument to data, so data is actually the error (usually null on success), and the real file contents are dropped. It must be (err, data) => { if (err) ...; }.
The Node.js async_hooks module fires instance-method callbacks in a well-defined lifecycle for a single async resource, from creation through destruction. Place these callbacks in the exact order they fire for one async operation (earliest first).#
Put these in order
Show answer
The async_hooks lifecycle order is init → before → after → destroy. init fires when the resource is created, before just before its callback runs, after when the callback returns, and destroy when the resource is retired. This sequence is defined by the Node.js async_hooks documentation and is the foundation on which AsyncLocalStorage propagates context across async boundaries.
The Node.js async_hooks documentation defines a strict lifecycle: init fires synchronously when the resource is created (e.g., a timer is scheduled or a promise is resolved); before fires immediately before the runtime begins executing the resource's callback; after fires immediately after the callback returns; and destroy fires when the resource is garbage-collected or explicitly destroyed. before/after can repeat if the same resource's callback fires more than once, but init always comes first and destroy always comes last.
The following Node.js function reads a file and parses it as JSON using the error-first callback convention. When fs.readFile fails (e.g., the file does not exist), the callback is invoked twice — once with the file-read error and again with a JSON parse error. Which line contains the root-cause bug?#
const fs = require('fs');
function readJsonFile(filePath, callback) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
callback(err);
}
try {
const parsed = JSON.parse(data);
callback(null, parsed);
} catch (parseErr) {
callback(parseErr);
}
});
}Options
Show answer
Line 6 — callback(err) is not preceded by return, so execution falls through to JSON.parse(data) after the callback is already invoked.
When fs.readFile fails, err is truthy, so line 6 calls callback(err) — the first invocation. Because there is no return on line 6, execution continues to line 9 where JSON.parse(data) is called with data === undefined (no file content was read). This throws a SyntaxError, which is caught on line 11, and line 12 calls callback(parseErr) — the second invocation. The error-first callback contract requires exactly one call; adding return before callback(err) on line 6 (return callback(err);) stops the fall-through. The claim that JSON.parse(data) should be JSON.parse(data.toString()) is wrong because passing 'utf8' as the encoding guarantees data is a string, not a Buffer. The claim that callback(null, parsed) is not preceded by return is wrong because line 10 is the last statement in its try block — nothing executes after it, so no return is needed. The claim that fs.readFile should be replaced with fs.readFileSync is wrong because switching to a synchronous API does not fix a control-flow bug and would block the event loop.
What problem does AsyncLocalStorage solve, and how does it work at a high level?#
Show answer
AsyncLocalStorage (from node:async_hooks) provides per-request context that survives across asynchronous boundaries — callbacks, promises, and awaits — without threading a value through every function argument. You call store.run(context, callback) to establish a context, and any code executing within that asynchronous call chain can retrieve it with store.getStore(). It is commonly used to carry a request id or trace id through a whole request lifecycle for logging.
AsyncLocalStorage ties a store to an asynchronous execution context so values set with run() are readable via getStore() anywhere downstream in that chain, even after await. It is the idiomatic way to propagate request-scoped data (request/trace ids, user) without passing it explicitly everywhere.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See nodejs salaries and hiring demand from live job postings.
The other 9 questions
This page shows 10 and marks what you pick. That's as far as a page can go. A free account opens the other 9 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 answers per 30 days, each marked with written feedback