52 questions

JavaScript interview questions and answers

Junior questions

  1. 1. What is the difference between var, let and const?

    Answer

    var is function scoped, while let and const are block scoped. A var can be redeclared, let and const cannot be redeclared in the same scope. A const cannot be reassigned, but the contents of an object or array stored in it can still change. Modern code uses const by default and let only when the value changes.

    Follow-up question: What is the temporal dead zone (TDZ)?

  2. 2. What is hoisting in JavaScript?

    Answer

    When a scope is created, the engine registers declarations before any code runs. Function declarations are hoisted completely, so you can call them before the line where they are declared. A var is hoisted with the value undefined. let and const are registered too, but until their declaration line they sit in the TDZ, and accessing them throws a ReferenceError.

    Follow-up question: Is a function expression assigned to a const hoisted?

  3. 3. What is the difference between == and ===?

    Answer

    === compares without type coercion: the values must have the same type and be equal. == first coerces the operands to a common type, so 0 == "" and null == undefined are both true. Because the coercion rules are surprising, === is used almost everywhere. One exception sometimes kept on purpose: x == null checks for both null and undefined at once.

    Follow-up question: Why is NaN === NaN false, and how do you check a value for NaN?

  4. 4. What is a closure? Give an example of how it is used.

    Answer

    A closure is a function together with the lexical environment it was created in. It can access the outer function's variables even after that function has returned. This is how you build private state, for example a counter that returns an increment function. Callbacks, function factories and memoization also rely on closures.

    Follow-up question: What does for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)) print, and how do you fix it?

  5. 5. How does a shallow copy of an object differ from a deep copy?

    Answer

    A shallow copy creates a new top level object, but nested objects remain shared references. You make one with spread { ...obj } or Object.assign. A deep copy recursively copies every level, so changes to nested data do not affect the original. The built in structuredClone does this, while JSON.parse(JSON.stringify(obj)) loses functions, undefined and dates.

    Follow-up question: What can structuredClone not copy?

  6. 6. How does an object differ from an array in JavaScript?

    Answer

    An object is a set of named fields, for example user.name. An array is an ordered list where you get an element by its number, and the first one has number zero. On the backend you most often see an array of objects, such as a list of orders. Technically an array is also an object, but with special behavior for indexes and the length property.

    Follow-up question: How do you get the total field of the second element of the orders array?

  7. 7. Given const a = { n: 1 }; const b = a; b.n = 2;, what does console.log(a.n) print and why?

    Answer

    It prints 2. The line const b = a does not copy the object, it gives one more reference to the same object. So a change through b is visible through a. Primitives (numbers, strings, booleans) are copied by value, while objects and arrays are passed by reference. To get an independent copy you need spread or structuredClone.

    Follow-up question: How do you make an independent copy of a nested object?

  8. 8. What is a callback in simple words? Give an example.

    Answer

    A callback is a function I pass to another function so that it calls it itself at the right moment. For example, onClick calls my function on a click, and map calls it for each array element. Asynchronous APIs work the same way: I do not wait for the result, I give a function that will run when it is ready.

    Follow-up question: How many times will map call the callback for an array of three elements?

  9. 9. The code prints A, then schedules setTimeout(() => console.log('B'), 0), then prints C. In what order do the letters appear and why?

    Answer

    It prints A, then C, then B. First all the synchronous code of the current call runs. The setTimeout callback goes to a queue and runs only when the stack is empty, even if the delay is zero. So zero in setTimeout means 'as soon as possible after the current code', not 'immediately'.

    Follow-up question: Does setTimeout with zero guarantee that the function runs exactly instantly?

  10. 10. An async function getUser returns the string 'Olya'. What does console.log(getUser()) print in Node.js and how do you get the string itself?

    Answer

    It prints Promise { 'Olya' }, because an async function always returns a promise, even if there is a plain return inside. To get the string you write await getUser() inside an async function or use .then. A forgotten await is one of the most common mistakes: the code compiles but then works with a promise instead of data.

    Follow-up question: What does the word await do to the rest of the program: does it stop it or not?

  11. 11. What do typeof null and typeof NaN return? How do isNaN and Number.isNaN differ, and how do you filter out an invalid number from a query parameter?

    Answer

    typeof null gives 'object': it is a historical mistake of the first implementation that cannot be fixed, so an object is checked as value !== null && typeof value === 'object'. typeof NaN gives 'number'. The global isNaN('abc') first converts the argument to a number and returns true, while Number.isNaN does no conversion and is true only for NaN itself. For a query parameter prefer Number.isFinite(Number(x)), remembering that Number('') gives 0 and parseInt('42px') gives 42.

    Follow-up question: Why does [NaN].includes(NaN) give true while indexOf returns -1?

Middle questions

  1. 1. How does the event loop work? How do microtasks differ from macrotasks?

    Answer

    JavaScript runs code on a single thread through the call stack, while asynchronous callbacks wait in queues. After each macrotask (a script, setTimeout, an event) the event loop drains the whole microtask queue, and only then takes the next macrotask. Microtasks are Promise.then callbacks, await continuations and queueMicrotask. That is why Promise.resolve().then(...) runs before setTimeout(..., 0).

    Follow-up question: What happens if a microtask keeps scheduling another microtask?

  2. 2. What determines the value of this inside a function?

    Answer

    In a regular function this depends on how it is called: for obj.method() it is obj, for a plain call it is undefined in strict mode or the global object without it. call, apply and bind set this explicitly, and new binds it to the newly created object. An arrow function has no this of its own and takes it from the enclosing scope. That is why a method passed as a callback often loses its context.

    Follow-up question: How do you keep this when passing a class method to addEventListener?

  3. 3. How do promises relate to async/await? How do you handle errors and parallel requests?

    Answer

    async/await is syntax on top of promises: an async function always returns a promise, and await waits for a result without nested then calls. Errors are caught with try/catch around await or with .catch in a chain. Consecutive await calls run one after another, so independent requests are started together with Promise.all. If you need the results even when some fail, use Promise.allSettled.

    Follow-up question: How does Promise.race differ from Promise.any?

  4. 4. A function has console.log(x) and below it let x = 'inner', while the outer x equals 'outer'. What does the call print and why is the outer value not used?

    Answer

    You get ReferenceError: Cannot access 'x' before initialization. The inner let x is registered in the creation phase of the function context and shadows the outer x for the whole block, not only after its line. From the start of the block to the initialization line the variable is in the temporal dead zone, and any access throws. That is why typeof of an undeclared variable is safe, while typeof of a variable in the TDZ also throws.

    Follow-up question: Do default parameters have a TDZ? What happens with function f(a = b, b = 1) {} when you call f()?

  5. 5. A for (var i = 0; i < 3; i++) loop schedules a setTimeout that prints i. What does it print, and what does a similar loop with let in the header print? Why does a let declared outside the loop behave like var?

    Answer

    With var you get 3, 3, 3, because there is one variable for the whole function and the callbacks fire after the loop has finished. With let in the header you get 0, 1, 2: the specification creates a new binding for each iteration. If let k is declared outside for, there is one binding again and the result is like var. A closure captures the variable, not its value at the moment of creation.

    Follow-up question: How do you fix the var version without let?

  6. 6. Two arrow functions are created in one function: one returns x, then x = 2, the other also returns x. What does each return? And where is a name looked up: where the function is written or where it is called?

    Answer

    Both return 2, because they close over the same variable x, not its value at creation time. A closure holds a reference to the lexical environment, not a snapshot of values. Names are resolved lexically: the environment where the function was created is stored in an internal slot, and calling it from elsewhere does not change that. So a variable of the calling function is not visible to the inner function, because JavaScript has no dynamic scope.

    Follow-up question: How do eval and with break lexical scope, and why does the engine give up optimizations because of them?

  7. 7. A class has a method key(id) that reads this.prefix. Why does ids.map(service.key) fail with a TypeError, while ids.map((id) => service.key(id)) works?

    Answer

    this is determined by how a function is called, not where it is declared. The expression service.key(id) is a call through an object, so this equals service. But service.key without parentheses just reads a function that remembers nothing about service, and map calls it as a plain function. A class body is always strict, so this there is undefined, and reading prefix throws a TypeError. Fixes: bind, an arrow wrapper, or the second thisArg argument of map.

    Follow-up question: Why does autobind in the constructor solve this for the whole class, and what does it cost?

  8. 8. How does an arrow function differ from a regular one with respect to this? What happens to this in a regular function inside setTimeout in Node.js, and in a method destructured from an object?

    Answer

    An arrow function has no this of its own, it takes it from the surrounding scope where it is written. In CommonJS at the top level that is module.exports, in ESM it is undefined. A method pulled out by destructuring is called without an object: in sloppy mode this is globalThis, in strict mode undefined. Node calls a setTimeout callback with this equal to the Timeout object, in a browser it would be window. An arrow inside a method takes the this of that method.

    Follow-up question: Why can an arrow not be used as a constructor, and what happens with new on an arrow?

  9. 9. A prototype holds an array tags and a number count. One descendant does tags.push('x') and count++. What will another descendant see, and why do the array and the number behave differently?

    Answer

    Reading a property walks the prototype chain, while assignment always creates or changes an own property of the object. a.tags.push('x') only reads the array from the prototype and mutates it, so that array is shared and the other descendant will see 'x'. a.count++ expands to a.count = a.count + 1: it reads 0 from the prototype and writes its own count = 1, so the other descendant still has 0. Mutable state must not be placed on a prototype.

    Follow-up question: What happens if count on the prototype is made writable: false and count++ is repeated in strict mode?

  10. 10. How does prototype differ from __proto__? Which two prototype chains does class Dog extends Animal build?

    Answer

    Every object has an internal slot [[Prototype]]. __proto__ is a deprecated accessor on Object.prototype that reads it, the modern way is Object.getPrototypeOf. prototype is a regular property of a constructor function: the object that new will put into [[Prototype]] of a new instance. extends builds two chains: the instance chain (d, Dog.prototype, Animal.prototype, Object.prototype) and the constructor chain (Dog, Animal), so that static methods work. Class methods are not own properties of the instance: in sees them, Object.hasOwn does not.

    Follow-up question: What does Object.setPrototypeOf do to an existing object, and why does MDN advise avoiding it?

  11. 11. They say class is syntactic sugar over constructor functions. What differences between them still exist?

    Answer

    The mechanics are similar: both a class and a function have a prototype where methods go. But class methods are non-enumerable, while Fn.prototype.m = ... creates an enumerable property, so for...in over an old-style instance also lists inherited methods. A function can be called without new, and this becomes globalThis in sloppy mode, while a class without new throws a TypeError. A class body is always strict, and a class is not hoisted like a function declaration (it sits in the TDZ).

    Follow-up question: Why did inheriting from Error through functions break instanceof and stack before ES2015?

  12. 12. An async function prints a1, does await null, prints a2. Right after calling it there is Promise.resolve().then(p1).then(p2) and a synchronous console.log('sync'). In what order does everything print, and how many microtask steps does await cost?

    Answer

    Order: a1, sync, a2, p1, p2 (the continuation of the async function is queued before p1). The code of an async function up to the first await runs synchronously, so a1 is first. Each await of a plain value puts the continuation into the microtask queue and costs one step. The following then handlers are registered only after the previous ones run, so the async continuation and the then chain interleave.

    Follow-up question: How does the order change if you write await new Promise((r) => r()) instead of await null?

  13. 13. In Node.js there are two setTimeout calls with zero delay. The first schedules Promise.then and queueMicrotask, the second Promise.then and process.nextTick. Do the microtasks of the first timer run before the second timer? What goes first: nextTick or a promise?

    Answer

    Yes, the microtasks of the first timer run before the second timer. Since Node 11, after each individual timer callback Node drains the process.nextTick queue first, then the microtask queue, like a browser after each task. queueMicrotask and Promise.then share one FIFO queue. The nextTick queue is separate and is processed before promise microtasks, so in the second timer nextTick runs before then, even though the promise was created first.

    Follow-up question: What happens to timers if a microtask recursively schedules itself through queueMicrotask?

  14. 14. A chain: a then without return, then a then that throws, then another then, then a catch that returns 42, then a then and a finally. What happens at each step?

    Answer

    Every then returns a new promise, and its state is set by the callback. Without return the next step gets undefined: this is the most common mistake in chains. A throw in a callback makes the next promise rejected, and a then without onRejected passes it on, so the intermediate then does not run. A catch that returns a value restores the chain to fulfilled, and the next then gets 42. finally receives no value and does not change it unless it throws itself.

    Follow-up question: What changes if catch is placed first in the chain?

  15. 15. How do Promise.all, allSettled, race and any differ? What backend tasks do you use each for, and what happens to the remaining operations after all rejects?

    Answer

    all rejects on the first error, race on the first settlement of any kind, any waits for the first success (if all fail, it rejects with AggregateError), and allSettled waits for all and never rejects. Typical use: all for dependent requests, allSettled when partial results are needed, any for mirrors, race for timeouts. Importantly, after rejection the remaining operations are not cancelled: requests and SQL continue and hold connections. Cancellation is done separately through AbortController.

    Follow-up question: How do you write a request timeout so that the request really stops and the connection returns to the pool?

  16. 16. An endpoint enriches 2000 orders with data from an external service. The version with await in a for loop takes 40 seconds, the version with Promise.all gets 429 and a DB pool timeout. What is wrong with both, and how do you do it right?

    Answer

    The sequential version waits for each step: 2000 times 20 ms is 40 seconds. Promise.all starts all 2000 operations at once: the service rate limits requests, and a pool of 10 connections is clogged by a queue. You need bounded concurrency: a worker pool, p-limit or batches of 10-20. Also reduce the number of calls: unique ids, a batch endpoint, one INSERT per batch. Partial errors are collected with allSettled instead of failing the whole request.

    Follow-up question: How do you write mapLimit(items, limit, fn) without libraries?

  17. 17. When does the body of a generator start running? What happens if a for...of loop over a generator is interrupted with break?

    Answer

    Calling a generator function does not run the body, it starts on the first next(). After that the generator runs only up to the next yield, so the computation is lazy and alternates with the consumer. break in for...of calls the iterator's return(): the generator is closed and yields nothing more, and code after the current yield does not run. Only finally blocks run, so cleanup of resources in a generator is written there.

    Follow-up question: What changes if the loop in the generator is wrapped in try/finally with a cleanup log?

  18. 18. How does an iterable differ from an iterator? Why can your own range collection be spread twice while a generator can be used only once?

    Answer

    An iterable has a Symbol.iterator method that returns an iterator. An iterator has a next() method that returns { value, done }. A range collection creates a new iterator on every call of Symbol.iterator, so it can be traversed many times. A generator returns itself from Symbol.iterator, so it is single use. Consequence: into a function that may walk the data twice you pass an iterable (or an array), not an already created generator.

    Follow-up question: How do you make a Paginated class so that for await...of over it loads the next API pages?

  19. 19. Why is 0.1 + 0.2 not equal to 0.3, and why does 1.005 round to 1? How would you handle money in a payment service?

    Answer

    Numbers in JavaScript are IEEE 754 doubles, and 0.1 has no exact binary representation, so the sum gives 0.30000000000000004. Fractions are compared through a difference with a tolerance, and Number.EPSILON only suits values near one. 1.005 * 100 gives 100.49999999999999, so Math.round gives 1 and toFixed does not help. Money is kept in whole minor units (cents), in BigInt or in a decimal library, and in Postgres you use numeric.

    Follow-up question: The driver returns numeric as a string. How do you carry the amount to JSON with no float at any step?

  20. 20. A frontend receives ids from a Postgres bigint in JSON. What bugs can this cause and how do you avoid them?

    Answer

    JSON.parse turns a number into a double: 9007199254740993 silently becomes 9007199254740992, with no error. The limit of safe integers is Number.MAX_SAFE_INTEGER (9007199254740991), above it neighboring integers are indistinguishable. BigInt is exact, but JSON.stringify cannot handle it and throws a TypeError. So bigint ids are sent in JSON as strings; the pg driver returns int8 as a string by default, and you should not blindly parse it into a number.

    Follow-up question: How do you verify at the API contract level that no client receives a bigint as a JSON number?

  21. 21. Why does [] == false give true and null == 0 give false, although null >= 0 gives true? How does the == algorithm work?

    Answer

    If one operand is an object and the other a primitive, ToPrimitive is called first: [] becomes the string ''. A boolean always becomes a number first, so false is 0 and '' also becomes 0, hence true. null == x is true only for null and undefined, while the >= operator converts null to 0, so null >= 0 is true. NaN is not equal to anything, even itself. Object.is distinguishes NaN and -0, unlike ===.

    Follow-up question: Why does [NaN].includes(NaN) give true while indexOf returns -1?

  22. 22. What does a copy via JSON.parse(JSON.stringify(x)) lose, what can structuredClone not do, and what would you use to copy a DTO before mutation?

    Answer

    A JSON copy turns Date into a string, Map into {}, drops undefined, and turns NaN into null. structuredClone keeps Date, Map, Set, NaN, undefined and circular references. But it does not copy functions (it throws DataCloneError) and does not preserve class prototypes, getters and private fields. Spread is only a shallow copy: nested objects stay shared. For a plain data DTO structuredClone is usually the best choice.

    Follow-up question: What happens to an instance of class Money with a private field #cents after structuredClone?

  23. 23. A default config is frozen with Object.freeze, a service makes a copy with spread and changes the nested timeout. Why did another service suddenly get a changed timeout?

    Answer

    Spread copies only the first level, so the nested object in the copy and in the original is the same object. Object.freeze is also shallow: the nested object is not frozen, so the change goes through. In strict mode writing to a frozen property throws a TypeError, in sloppy mode it is silently ignored. Fixes: deep freeze, a factory that returns a new object each time, or structuredClone at creation.

    Follow-up question: How does Object.freeze differ from Object.seal and Object.preventExtensions?

  24. 24. A client complains that in a JSON response the key '2026' comes before 'total', although the server order was different. In what order does JavaScript enumerate object keys?

    Answer

    First come keys that are array indexes (integers from 0 to 2^32 minus 2), in ascending order. Then string keys in insertion order, and '-1' or '01' are not indexes, so they are ordinary strings. Symbols come last, and Object.keys and JSON.stringify do not show them. If order matters for arbitrary keys, use a Map: it keeps insertion order for any keys, including numbers.

    Follow-up question: How does V8 store indexed properties separately from named ones, and why is an array with holes slower?

  25. 25. How does debounce work, and why does an implementation that does not keep the timer in a closure fail? How does debounce differ from throttle?

    Answer

    Debounce delays a call until an interval has passed with no new calls. A correct implementation keeps timer in a closure and calls clearTimeout(timer) before the new setTimeout. Without cancelling, every call sets its own timer, and saving runs for every keystroke. Throttle instead calls the function no more than once per interval. In production you also pass this and the arguments of the last call, and add cancel, flush and leading or trailing modes.

    Follow-up question: How do you implement throttle so that the last call of a series is not lost (trailing)?

  26. 26. In a config retries: 0 means 'do not retry', but the service makes 3 attempts. How does ?? differ from ||, and where does ?. help and where does it hide an error?

    Answer

    || substitutes the default for any falsy value: 0, '', false, NaN. ?? substitutes only for null and undefined, so retries ?? 3 keeps 0. ?. short-circuits the whole chain if the left side is null or undefined and returns undefined without a TypeError. obj.method?.() will not call the method if it is missing, but throws a TypeError if it is not a function. Excessive ?. hides broken invariants: it is better to fail early than to carry undefined further.

    Follow-up question: How does ??= differ from ||= in the example of lazy cache initialization?

  27. 27. A client sends { offset: null, sort: '' }, and the destructuring has default values. Why did the defaults not apply and the database query fail?

    Answer

    A default in destructuring and function parameters applies only to undefined, while null and '' pass through as they are. = {} for the whole parameter protects against a call without arguments, but not against null: destructuring null throws a TypeError. JSON has no undefined: a missing key gives undefined, while null arrives as null. So defaults and normalization are done at the system boundary: with a validation schema or an explicit ??.

    Follow-up question: Why does @IsOptional() in class-validator let null through, and how does that affect PATCH where null means 'clear the field'?

  28. 28. How do you correctly wrap errors without losing the root cause? Why does a custom error class show name equal to Error, and what will a log show after JSON.stringify(error)?

    Answer

    new Error(message, { cause }) keeps the root cause, and the chain is not lost when wrapping. A subclass without this.name = ... inherits name = 'Error' from Error.prototype, although instanceof works. message, stack and cause are not enumerable, so JSON.stringify yields only own enumerable fields, most often {}. A logger (pino, console) must serialize the error explicitly, recursively together with cause.

    Follow-up question: How do you tell an expected domain error from a bug in an error filter so the stack is not sent to the client?

  29. 29. A reducer returns a new object via spread, but changes 'leak' into the old state. Where does the mutation usually hide?

    Answer

    sort() sorts the array in place and returns the same array, so the old state is changed. Also, sort without a comparator compares strings: [10, 9, 1] becomes [1, 10, 9]. Spreading the outer object does not protect the nested object: a write to s.user.name is visible in the old state too. Fixes: toSorted, toReversed, with, copying every level that changes, or Immer.

    Follow-up question: What is structural sharing and why is copy-on-write cheaper than a deep copy on every change?

Senior questions

  1. 1. What happens in the engine when a function is called? What is in an execution context, how does the creation phase differ from the execution phase, and how is this related to the call stack?

    Answer

    A call creates an execution context and puts it on top of the context stack (the call stack). The context contains LexicalEnvironment, VariableEnvironment, the this binding, the function itself and the realm. In the creation phase the parameter bindings appear, var with the value undefined, function declarations in full, and let, const and class in the TDZ. In the execution phase the code runs line by line, and after return the context is popped off the stack. A function's environment has an outer reference to the environment where the function was written: this is the basis of closures.

    Follow-up question: How does LexicalEnvironment differ from VariableEnvironment in the example of a block with let inside a function?

  2. 2. The code holds only one theThing, but heapUsed grows to hundreds of megabytes, although the helper function unused is never called. Why does this happen with closures, and how do you find it in a heap snapshot?

    Answer

    V8 creates one Context object per scope, shared by all closures that capture something from it. The method closure captures the same Context as unused, and it holds prev (the previous theThing) and big. A chain forms: every new theThing holds the old one through prev, and that one holds its own big. In a heap snapshot you see this in Retainers: context and prev. Fix: null out prev before leaving the scope or move unused into a separate scope.

    Follow-up question: Why would WeakRef or WeakMap not save this case, and what exactly should be a weak reference here?

  3. 3. Rank the this binding rules by priority. What happens with new on a bound function and with repeated bind or call? How does this change in strict mode?

    Answer

    Priority from highest: new, then explicit binding (call, apply, bind), then a call through an object, then default binding. Repeated bind and call do not change this of an already bound function: the first bind wins. new has priority even over bind: a new object is created. In sloppy mode a primitive this is wrapped into an object, and null and undefined are replaced with globalThis. In strict mode this is passed as is.

    Follow-up question: What is new.target for in base classes, and how does it behave with a bound function?

  4. 4. An endpoint merges the request body into a settings object with a recursive function, and after one request all users got admin rights. How does prototype pollution work and how do you close it?

    Answer

    JSON.parse creates an own property with the key __proto__, and for...in walks it. In merge, the expression target['__proto__'] returns Object.prototype through the accessor, so the function writes into the global prototype. After that all objects inherit, for example, isAdmin: true. Defense: skip the keys __proto__, constructor, prototype, use Object.create(null) or Map, validate the body with a schema, and if needed disable __proto__ at the Node process level.

    Follow-up question: Why is changing Object.prototype dangerous for libraries that read options from a plain empty object?

  5. 5. An async function does return Promise.resolve('r'), while other then chains run alongside. Why is r printed later than expected, although the promise is already fulfilled?

    Answer

    Returning a plain value from an async function fulfills its promise immediately. Returning a promise calls resolve with a thenable: the specification queues NewPromiseResolveThenableJob, which calls then on the inner promise. This costs two extra microtask queue steps, so the result arrives later than in neighboring chains. The same applies to resolve(promise) in new Promise and to returning a promise from then.

    Follow-up question: If you replace return Promise.resolve('r') with return await Promise.resolve('r'), where will r be and why?

  6. 6. A colleague says an import loop does not block the server because it has await. But during the import the health check times out. Why, and how do you fix it?

    Answer

    await of an already ready value continues through a microtask, and the microtask queue is drained before any macrotask. So such a loop never yields control to the event loop: timers and I/O wait until the whole loop finishes. The event loop does not reach the timers and poll phases, so the health check and other requests stand still. Fix: periodically yield through setImmediate or scheduler.yield, split the work into batches or move it to a worker. process.nextTick would not help here.

    Follow-up question: Why would process.nextTick instead of setImmediate change nothing?

  7. 7. How does the browser event loop differ from the Node.js event loop? What does the language define and what does the environment define?

    Answer

    ECMAScript defines only jobs (microtasks) for promises, while the loop itself is set by the environment: the HTML specification for the browser and libuv for Node. In a browser it is one task, then a microtask checkpoint, then a possible render with requestAnimationFrame. Node has the phases timers, pending, poll, check, close, and microtasks after each callback. process.nextTick and setImmediate exist only in Node, and nextTick runs ahead of promise microtasks. Consequence: endless microtasks block both rendering and I/O.

    Follow-up question: How do you measure in Node that the event loop is lagging, and what lag value is already alarming?

  8. 8. Two async functions both have try/catch, one does return fail(), the other return await fail(). Why does only the second catch the error, and when is return await mandatory?

    Answer

    return fail() returns a promise, and try finishes before the promise rejects, so catch does not see the error. return await waits for the result inside try, and catch fires. The rule: return await is mandatory inside try/catch and try/finally, and it also keeps the function in the async stack trace. Returning a promise without await also costs extra queue steps.

    Follow-up question: What does the same mistake look like with finally that calls client.release() for a connection pool?

  9. 9. Why use a WeakMap in a metadata cache instead of a Map? Why can a string not be a key, and when are WeakRef and FinalizationRegistry appropriate?

    Answer

    A WeakMap key is compared by object identity and does not keep the object alive: when the object is no longer reachable anywhere else, the entry disappears with it. Keys can only be objects and non-registered symbols, a primitive throws a TypeError. A WeakMap is not iterable and has no size. WeakRef and FinalizationRegistry are non-deterministic: the callback may never be called at all, so an explicit close is needed to free resources. A cache will not help if the value refers back to the key: then the object stays reachable.

    Follow-up question: Why would a WeakMap cache not help if the cache value refers back to the same object?

  10. 10. An index by id from 20,000 rows is built with rows.reduce((acc, r) => ({ ...acc, [r.id]: r }), {}) and takes hundreds of milliseconds, while a simple loop is almost instant. Why, and how do you fix it without giving up purity?

    Answer

    Each iteration copies all accumulated keys, so the total is O(n²): for 20,000 rows that is about 200 million property copies. In addition thousands of intermediate objects appear for the GC. The event loop is blocked for the whole time, and the p99 of all other requests of the process suffers. Mutating a local accumulator inside a function does not break purity, because there is no outside effect. So prefer a loop, a Map or Object.fromEntries.

    Follow-up question: How do you find such a hot reduce in production: which Node tools would you use to profile the CPU?

  11. 11. A memoize built on Map does not hit for identical object arguments and returns a stale value after the object is mutated. How do you make memoize for object arguments correctly?

    Answer

    Map compares objects by reference, so two literals { qty: 2 } are two different keys. And mutating a key object does not change the cache entry, so the function returns the old result. The key is made from a stable primitive (an id, a hash), canonical JSON with sorted keys, or you use a WeakMap by identity for immutable objects. The cache must be bounded (LRU, TTL) and the function pure, otherwise memoize causes a memory leak and wrong data.

    Follow-up question: How do you cache a promise so that an error does not get stuck in the cache and concurrent requests merge into one?

  12. 12. An async generator takes a connection from the pool and yields cursor rows, and under load the pool is exhausted. Where does the connection leak and how do you fix it?

    Answer

    finally in a generator runs only when it finishes: on done, throw or a call to return(). If the consumer takes values with a manual next() and abandons the iterator, the generator stays suspended forever and the connection is never returned. Fix: consume with for await, and with manual calls always call return() in the consumer's finally, or use await using with Symbol.asyncDispose. As a safety net you need a pool idle timeout and a metric of busy connections.

    Follow-up question: How do you write a wrapper take(asyncIterable, n) that is guaranteed to close the source?

Train these questions

Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.

Answer out loud and get graded

Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.

The app is coming to the stores soon.

In the knowledge base

Reference pages for the technologies on this topic: who maintains them, when to use them and what interviews ask.

Other topics