This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.
Identity, Image, and Privacy Notice
To respect individual privacy, some names, profile photographs, avatars, biographical details, and other identifying information displayed in this guide may be replaced with pseudonyms, licensed stock images, illustrative avatars, composite images, or representative descriptions. Unless a person is expressly identified as an actual contributor, a displayed name, image, or profile should not be understood as depicting or identifying a specific candidate, interviewer, employee, or other real individual. These representations are provided for editorial and illustrative purposes only and do not imply endorsement, employment, participation, or affiliation with this guide or any company mentioned in it. Any resemblance to an actual person is coincidental.
Company Notice
This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
51. Implement a function that calculates the arithmetic mean of an array.CodingEasy
i Question Details
Write mean(values). The input is an owned JavaScript array of one or more finite numbers; duplicates and negative values are allowed, order is irrelevant, and the function must not mutate the array. Return the numeric arithmetic mean. Reject a non-array or any non-finite member with TypeError; reject an empty array with RangeError. Use only standard ECMAScript features and do not call an external statistics library. Example: mean([2, -1, 5, 2]) must return 2. A single pass is required, with O(n) time and O(1) auxiliary space.
Short Interview Answer (30-60 seconds)
I would first validate that the input is a non-empty array. Then I make one pass through it while keeping a running sum and count. For each number, I verify that it is finite before adding it to the sum and increasing the count. At the end, I return sum divided by count. This works because every valid number contributes exactly once. The time complexity is O(n), and the auxiliary space complexity is O(1).
The function receives an array containing one or more finite numbers and returns their arithmetic mean. The mean is the total of all numbers divided by how many numbers there are. Duplicate and negative values are allowed. The function must not change the array. It must reject a non-array or any non-finite member with TypeError and reject an empty array with RangeError. A single pass with a running sum and count satisfies these requirements.
Useful Questions to Ask the Interviewer
Should a non-array input throw TypeError?
Should an empty array throw RangeError?
Should NaN, Infinity, -Infinity, or other non-finite members throw TypeError?
Must the original array remain unchanged?
How to Explain It in an Interview
1. Validate the input
First, I check Array.isArray(values). If values is not an array, I throw TypeError. Next, I check values.length === 0. If the array is empty, I throw RangeError. These checks happen before calculating the mean.
2. Initialize the running state
I set sum = 0 and count = 0. The variable sum stores the total of all valid numbers processed so far. The variable count stores how many numbers have been processed. Before the loop starts, no numbers have been processed, so both values are 0.
3. Validate each member and accumulate
I process the array from left to right with one for...of loop. For each number n, I check typeof n !== 'number' or !Number.isFinite(n). If either condition is true, I throw TypeError. Otherwise, I add n to sum and increase count by 1. The array itself is never changed.
4. Walk through the exact example
For mean([2, -1, 5, 2]), the starting state is sum = 0 and count = 0.
Step 1: n = 2. It is finite. I add 2, so sum becomes 2 and count becomes 1. At this point, sum / count is 2 / 1 = 2.
Step 2: n = -1. It is finite. I add -1, so sum becomes 1 and count becomes 2. At this point, sum / count is 1 / 2 = 0.5.
Step 3: n = 5. It is finite. I add 5, so sum becomes 6 and count becomes 3. At this point, sum / count is 6 / 3 = 2.
Step 4: n = 2. It is finite. I add 2, so sum becomes 8 and count becomes 4. At this point, sum / count is 8 / 4 = 2.
After the loop, the function returns sum / count, which is 8 / 4 = 2.
5. Explain why the result is correct
After every completed iteration, sum equals the total of all array members processed so far, and count equals the number of members processed so far. Each valid number is added exactly once. Therefore, when the loop finishes, sum is the total of the whole array and count is its length. Dividing sum by count gives the arithmetic mean.
6. Explain the JavaScript implementation
The implementation performs the array and empty-array checks first. It then initializes sum and count. The for...of loop processes each member once. Each member is checked with typeof and Number.isFinite before it affects the running state. Valid values update sum and count. Finally, the function returns sum / count.
7. Explain complexity and edge cases
The loop processes n array elements once, so the time complexity is O(n). Only a fixed number of variables are used, so the auxiliary space complexity is O(1). Non-array input causes TypeError. An empty array causes RangeError. NaN, Infinity, -Infinity, and any other non-finite member cause TypeError. Duplicates, negative values, zero, and a one-element array work correctly. The function does not mutate the input array.
Key Insight / Why This Solution Works
Use a single-pass running-sum algorithm. First validate that values is an array and that it is not empty. Initialize sum = 0 and count = 0. Then process each number n in order. Before using n, verify that it is a number and that Number.isFinite(n) is true. Add each valid n to sum and increment count. Finally, return sum / count. The central invariant is that after each completed iteration, sum is the total of exactly the values processed so far and count is exactly how many values have been processed.
Code
functionmean(values) {
// Reject anything that is not an actual JavaScript array.if (!Array.isArray(values)) {
thrownewTypeError('values must be an array');
}
// The problem requires at least one number.if (values.length === 0) {
thrownewRangeError('Array must contain at least one number');
}
// sum is the total of processed values.// count is the number of processed values.let sum = 0;
let count = 0;
// Process each array member once without changing the array.for (const n of values) {
// Reject non-number values, NaN, Infinity, and -Infinity.if (typeof n !== 'number' || !Number.isFinite(n)) {
thrownewTypeError('All array elements must be finite numbers');
}
// Update the running total and processed-item count.
sum += n;
count += 1;
}
// Arithmetic mean = total sum / number of values.return sum / count;
}
// Run the exact example from the diagram.console.log(mean([2, -1, 5, 2])); // 2
Time & Space Complexity
Let n be the array length. The function visits each member once, so the time complexity is O(n). It does not create another array or any collection that grows with the input. It only keeps sum, count, and the current loop value, so the auxiliary space complexity is O(1). The original input array is only read and is not modified.
Where it is used
This running-sum pattern is useful when software needs a simple average, such as average response time, temperature, score, latency, or sensor reading. The same idea also works well when values are processed one at a time because the program only needs to keep a running total and count.
Why Interviewers Ask This
This question checks whether a candidate can turn a simple mathematical rule into careful JavaScript. The interviewer can evaluate input validation, handling of NaN and Infinity, correct exception types, non-mutation of the input, a clean single-pass loop, and accurate complexity analysis. It also shows whether the candidate can explain a simple invariant and keep the implementation consistent with the stated contract.
Common interview mistakes
A common mistake is checking only typeof n === 'number'. NaN and Infinity also have the JavaScript type number, so Number.isFinite is needed. Another mistake is forgetting the empty-array check, which would make the calculation invalid. Candidates may also divide by the wrong count, mutate the input unnecessarily, or make an extra copy of the array. Another mistake is giving the wrong complexity. This solution is O(n) time and O(1) auxiliary space.
Interview tip
Explain the invariant while you code: after every iteration, sum is the total of all values processed so far and count is the number of values processed so far. Then the final return sum / count follows directly.
Interviewer may ask next
How would the solution change if the numbers arrived one at a time instead of being stored in an array?
Keep the same running sum and count as persistent state. For every new finite number, add it to sum and increase count. The current mean is sum / count. The invariant stays the same, so correctness is preserved. Processing n received values takes O(n) total time, or O(1) work per new value, and O(1) auxiliary space. The tradeoff is that the running state must remain available between arrivals.
Can the auxiliary space be reduced below O(1)?
No meaningful asymptotic reduction is possible because O(1) already means the extra memory does not grow with the input size. The algorithm needs only a small fixed amount of state, mainly sum and count. It still runs in O(n) time and O(1) auxiliary space, without creating another collection.
52. Implement a dynamically queued asynchronous task runner with a concurrency limit.CodingHard
i Question Details
Implement class TaskRunner with constructor limit, method add(task, {signal} = {}), read-only activeCount and pendingCount, and close({cancelPending = false} = {}). task(signal) returns a value or promise, and add returns a promise for that task. Start queued tasks in FIFO order while never running more than limit; tasks may be added while others run. A synchronous throw is a rejection. Aborting a pending task removes it without starting; aborting a running task forwards cancellation through its signal but cannot force the task to stop. close rejects new additions and either drains or rejects pending work. Example with limit 2 and tasks A, B, and C must start A and B first, then start C as soon as either slot becomes free. Clean up abort listeners and continue scheduling after failures.
Short Interview Answer (30-60 seconds)
I would keep pending tasks in a FIFO queue and track the number of running tasks. I start work only while activeCount is below the concurrency limit. Each running task gets its own AbortController. A pending abort removes and rejects that task before it starts. A running abort only forwards cancellation through its signal. When any task settles, I free its slot and schedule the next queued task. Queue operations are O(1) amortized, with O(n) auxiliary space.
This class controls how many asynchronous tasks may run at the same time. Each call to add returns a promise for that task. Extra tasks wait in the order they were added. When a running task finishes or fails, the oldest waiting task starts. A waiting task may be cancelled before it starts. A running task receives an abort signal, but the runner cannot force it to stop. close stops new additions and either lets waiting work drain or rejects it.
Useful Questions to Ask the Interviewer
Should close() return a promise that resolves after all already accepted work has finished?
With cancelPending = true, should already running tasks continue? The approved design keeps them running and rejects only pending work.
For a running task, is cooperative cancellation through AbortSignal sufficient? The approved design assumes yes because the runner cannot forcibly stop arbitrary asynchronous JavaScript.
How to Explain It in an Interview
1. Understand the input and required output
The constructor receives limit, which is the maximum number of tasks allowed to run together. add(task, { signal }) accepts a function. The runner calls that function with an internal AbortSignal. The function may return a normal value or a promise. add returns a promise for that result. activeCount is the number of running tasks. pendingCount is the number waiting in the FIFO queue. close rejects future additions. It either drains pending work or rejects pending work depending on cancelPending.
2. Use a bounded FIFO scheduler
The main data structure is a FIFO queue. New pending tasks go to the back. The scheduler takes tasks from the front. The central invariant is that activeCount never becomes greater than limit. FIFO order is preserved because an older pending task is always selected before a newer pending task. Tasks may be added while other tasks are running.
3. Start tasks and handle completion
Whenever capacity is available, the scheduler removes the oldest pending task and starts it. Starting a task increments activeCount and creates an AbortController for that task. The task call is placed inside a promise chain. This converts a synchronous throw into a rejected promise. When the task resolves or rejects, its add promise receives the same result. Cleanup then decreases activeCount and immediately runs the scheduler again. A failure therefore does not block later tasks.
4. Walk through the verified example
The diagram uses limit = 2. Task A takes 300 ms. Task B takes 500 ms. Task C takes 400 ms. A, B, and C are added at time 0.
At t = 0 ms, A starts and activeCount becomes
B then starts and activeCount becomes
C cannot start because both slots are occupied, so C waits in the FIFO queue. pendingCount is 1.
At t = 300 ms, A finishes. Its slot becomes free. C is the oldest pending task, so C starts immediately. B and C are now running. activeCount stays 2 and pendingCount becomes 0.
At t = 500 ms, B finishes. C continues running. activeCount becomes 1.
At t = 700 ms, C finishes. activeCount becomes 0. The queue is empty and all three tasks are complete.
This gives the exact execution order shown in the diagram: A and B start first, then C starts as soon as A frees a slot.
5. Handle aborts and close
If an external signal aborts while a task is still pending, the runner removes that task from the queue and rejects its add promise. The task is never called. If the signal aborts after the task has started, the runner aborts the task's internal AbortController. The task receives signal.aborted and may stop cooperatively. The runner cannot force it to stop.
close({ cancelPending: false }) marks the runner closed, so later add calls reject. Already accepted work continues and the queue drains normally. close({ cancelPending: true }) instead rejects all tasks that are still pending. Running tasks are not forcibly stopped. The close promise resolves after no running or pending work remains.
6. Explain why it is correct
The scheduler checks capacity before every start, so activeCount cannot exceed limit. The queue always removes the oldest pending entry, so pending tasks start in FIFO order. Every running task releases exactly one slot when it settles. Cleanup schedules more work even after rejection, so failures cannot stall the runner. Pending aborts remove work before execution. Running aborts only forward the signal. These rules match every transition in the diagram.
7. Explain complexity and important edge cases
Adding a task, taking the oldest task, removing a known pending task, starting a task, and handling one task settlement use O(1) queue bookkeeping in the implementation below. Rejecting all pending work during close({ cancelPending: true }) takes O(p), where p is the number of pending tasks. The runner stores pending entries and cancellation bookkeeping, so auxiliary space is O(n). Important cases are synchronous throws, rejected promises, abort before start, abort while running, adding after close, and repeated close calls.
Key Insight / Why This Solution Works
Use a bounded FIFO scheduler. Each pending task is a queue entry containing the task, its promise callbacks, its external AbortSignal, and queue links. The scheduler repeatedly starts the oldest pending entry while activeCount is less than limit. The central invariant is activeCount <= limit. FIFO is preserved because entries are added at the tail and removed from the head. Each settlement releases one slot and triggers scheduling again, so failures cannot stop the queue. A pending abort removes the entry before execution. A running abort is forwarded through that task's internal AbortController.
Code
classTaskRunner {
constructor(limit) {
// A positive integer gives us a clear number of concurrent slots.if (!Number.isInteger(limit) || limit <= 0) {
thrownewRangeError('limit must be a positive integer');
}
this.limit = limit;
// A doubly linked FIFO queue gives O(1) head removal and O(1)// removal of a known pending entry during cancellation.this._head = null;
this._tail = null;
this._activeCount = 0;
this._pendingCount = 0;
this._closed = false;
this._closeResolve = null;
this._closePromise = newPromise((resolve) => {
this._closeResolve = resolve;
});
}
// Read-only public count of tasks currently running.getactiveCount() {
returnthis._activeCount;
}
// Read-only public count of tasks waiting to start.getpendingCount() {
returnthis._pendingCount;
}
add(task, { signal } = {}) {
// close() permanently rejects later additions.if (this._closed) {
returnPromise.reject(newError('TaskRunner is closed'));
}
if (typeof task !== 'function') {
returnPromise.reject(newTypeError('task must be a function'));
}
returnnewPromise((resolve, reject) => {
const entry = {
task,
resolve,
reject,
externalSignal: signal,
controller: null,
onAbort: null,
state: 'pending',
prev: null,
next: null,
};
// If cancellation happened before add(), do not queue or start the task.if (signal?.aborted) {
reject(newDOMException('Aborted before start', 'AbortError'));
return;
}
entry.onAbort = () => {
if (entry.state === 'pending') {
// Pending abort: remove the task so it can never start.this._removePending(entry);
entry.state = 'settled';
reject(newDOMException('Aborted before start', 'AbortError'));
this._resolveCloseIfDone();
return;
}
if (entry.state === 'running') {
// Running abort: only forward cancellation to the task.// The task must cooperate with its AbortSignal.
entry.controller.abort();
}
};
if (signal) {
signal.addEventListener('abort', entry.onAbort, { once: true });
}
// New tasks join the back of the FIFO queue.this._enqueue(entry);
// Start as much queued work as the limit allows.this._tryRun();
});
}
_enqueue(entry) {
// Link the entry after the current tail.
entry.prev = this._tail;
entry.next = null;
if (this._tail) {
this._tail.next = entry;
} else {
this._head = entry;
}
this._tail = entry;
this._pendingCount++;
}
_removePending(entry) {
// Unlink one known pending entry in O(1) time.if (entry.prev) {
entry.prev.next = entry.next;
} else {
this._head = entry.next;
}
if (entry.next) {
entry.next.prev = entry.prev;
} else {
this._tail = entry.prev;
}
entry.prev = null;
entry.next = null;
this._pendingCount--;
}
_dequeue() {
// FIFO means the oldest pending entry is always at the head.const entry = this._head;
if (!entry) {
returnnull;
}
this._removePending(entry);
return entry;
}
_tryRun() {
// Never start more than limit tasks at the same time.while (this._activeCount < this.limit && this._pendingCount > 0) {
const entry = this._dequeue();
entry.state = 'running';
entry.controller = newAbortController();
this._activeCount++;
// Run the task through a promise chain.// A synchronous throw therefore becomes a rejection.Promise.resolve()
.then(() => entry.task(entry.controller.signal))
.then(
(value) => entry.resolve(value),
(error) => entry.reject(error)
)
.finally(() => {
entry.state = 'settled';
// The task no longer occupies a concurrency slot.this._activeCount--;
// Remove the caller's abort listener after settlement.if (entry.externalSignal && entry.onAbort) {
entry.externalSignal.removeEventListener('abort', entry.onAbort);
}
// Success and failure both free a slot for the next FIFO task.this._tryRun();
// A graceful close resolves after accepted work is finished.this._resolveCloseIfDone();
});
}
}
close({ cancelPending = false } = {}) {
// Make close idempotent. The first close call defines the shutdown mode.if (this._closed) {
returnthis._closePromise;
}
// From this point on, add() rejects new work.this._closed = true;
if (cancelPending) {
// Reject every task that has not started yet.while (this._pendingCount > 0) {
const entry = this._dequeue();
entry.state = 'settled';
if (entry.externalSignal && entry.onAbort) {
entry.externalSignal.removeEventListener('abort', entry.onAbort);
}
entry.reject(newError('TaskRunner closed before task started'));
}
} else {
// Already accepted work continues to drain normally.this._tryRun();
}
this._resolveCloseIfDone();
returnthis._closePromise;
}
_resolveCloseIfDone() {
// close() finishes only when no accepted task is running or pending.if (this._closed && this._activeCount === 0 && this._pendingCount === 0) {
this._closeResolve();
}
}
}
// Verified diagram example: limit = 2.// A runs for 300 ms, B for 500 ms, and C for 400 ms.
(async () => {
const runner = newTaskRunner(2);
const startedAt = performance.now();
constmakeTask = (name, ms) => (signal) =>newPromise((resolve, reject) => {
console.log(`${name} starts at about ${Math.round(performance.now() - startedAt)} ms`);
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
console.log(`${name} finishes at about ${Math.round(performance.now() - startedAt)} ms`);
resolve(name);
}, ms);
// This example task cooperates when its internal signal is aborted.constonAbort = () => {
clearTimeout(timer);
signal.removeEventListener('abort', onAbort);
reject(newDOMException('Aborted', 'AbortError'));
};
signal.addEventListener('abort', onAbort, { once: true });
});
// A and B start immediately. C waits because limit = 2.const a = runner.add(makeTask('A', 300));
const b = runner.add(makeTask('B', 500));
const c = runner.add(makeTask('C', 400));
console.log('After adding A, B, C:', {
activeCount: runner.activeCount,
pendingCount: runner.pendingCount,
});
// Expected scheduling:// t≈0: A and B start, C waits.// t≈300: A finishes and C starts.// t≈500: B finishes.// t≈700: C finishes.console.log('Results:', awaitPromise.all([a, b, c]));
// No new work is accepted after close().await runner.close();
})();
Time & Space Complexity
The queue implementation keeps direct previous and next links, so enqueue, dequeue, and removal of a known pending entry take O(1) time. Starting a task and handling one task settlement also use O(1) runner bookkeeping. close({ cancelPending: true }) must visit every pending task that it rejects, so that call takes O(p), where p is the number of pending tasks. The actual tasks may take any amount of time because their work is outside the scheduler. Auxiliary space is O(n) for queued entries, signals, listeners, and bookkeeping.
Where it is used
This pattern is useful when a frontend must limit parallel asynchronous work. Examples include API requests, file uploads, image processing, data prefetching, and background jobs. The concurrency limit prevents too much work from running at once. FIFO order gives predictable scheduling. AbortSignal support is useful when a user cancels an action, leaves a page, or no longer needs queued work.
Why Interviewers Ask This
This problem checks whether you can coordinate asynchronous work while protecting shared state. The interviewer is looking for correct FIFO scheduling, a clear concurrency invariant, promise handling, and safe state transitions. It also tests whether you understand the difference between cancelling pending work and forwarding cancellation to running work. Strong solutions handle synchronous throws, rejected promises, dynamic additions, listener cleanup, shutdown behavior, and failures without allowing the queue to stall.
Common interview mistakes
One mistake is starting every task immediately instead of checking activeCount against limit. Another is breaking FIFO order by taking a newer pending task first. A candidate may treat aborting a running task as forced termination, even though the runner can only forward an AbortSignal. It is also easy to forget that a synchronous throw must reject the promise returned by add. Another common bug is scheduling the next task only after success, which makes one rejection stall the queue. Finally, abort listeners should be removed when they are no longer needed.
Interview tip
State the two invariants before coding: activeCount never exceeds limit, and pending tasks start in FIFO order. Then show that every success, rejection, or synchronous throw reaches the same cleanup path, which releases one slot and schedules the next task.
Interviewer may ask next
How would you change the runner if close() should also request cancellation of tasks that are already running?
I would keep a collection of the AbortController objects for currently running entries. When close is called with an option such as cancelRunning, I would call abort() on each controller. Pending work could still be rejected according to cancelPending. The runner still cannot force a task to stop, so a task that ignores its signal may continue. Visiting the running controllers takes O(limit) time and storing them takes O(limit) extra space. The concurrency and FIFO invariants do not change.
What changes if tasks need priorities instead of strict FIFO order?
The concurrency-limit logic can stay the same, but the pending data structure must change because the next task is no longer simply the oldest one. A priority queue could choose the highest-priority pending task whenever a slot becomes free. Correctness would then mean respecting the priority rule rather than FIFO order. Enqueue and removal would usually become O(log n) with a heap, and auxiliary space would remain O(n). The tradeoff is more complex scheduling and the possibility that low-priority tasks wait much longer.
53. Render a constrained virtual-DOM tree into real DOM nodes.CodingHard
i Question Details
Implement renderVNode(vnode, documentRef = document). A text vnode is a string; an element vnode is {type, props, children}, where type is a lowercase HTML tag name, props may contain className, style as a plain property-value object, dataset as string values, Boolean attributes, ordinary string attributes, and event listeners named onClick, onInput, or onChange; children is an array of vnodes. Return a newly created Node without inserting it. Reject unknown event keys, innerHTML, dangerouslySetInnerHTML, invalid tag names, cyclic children, and non-string attribute values. Create text with createTextNode, use DOM properties only where specified, and attach listeners without evaluating strings. Example: {type:'button',props:{className:'save',disabled:true},children:['Save']} must produce a disabled <button class="save">Save</button> node. Preserve child order and support depth 10,000 without recursive stack overflow.
Short Interview Answer (30-60 seconds)
I would render the vnode tree with an explicit depth-first stack instead of recursion. Strings become text nodes with createTextNode. Element vnodes are validated, created with createElement, and their allowed props are applied safely. I keep an active-ancestor Set to detect only real cycles, then process each parent’s children from left to right. This preserves order and supports depth 10,000. The expected time is O(n + p), and auxiliary space is O(h).
This problem asks us to turn a small tree-shaped description into real browser nodes. A string becomes text. An object describes an HTML element, its settings, and its children. We must build one new node and return it without placing it on the page. We must reject unsafe or invalid input, keep children in the same order, and support very deep trees. The main idea is to build the tree with our own explicit stack, so depth 10,000 does not depend on JavaScript recursion.
Useful Questions to Ask the Interviewer
Should props and children always be present on every element vnode?
Should Boolean attributes be accepted only from the supported HTML Boolean-attribute set shown in the solution?
If the same vnode object is reused in two separate branches without forming a cycle, should that be allowed?
How to Explain It in an Interview
1. Understand the input and output
A text vnode is a string. An element vnode is an object with type, props, and children. The function returns one newly created DOM Node. It does not insert that node into the document. Text must use createTextNode. Elements must use createElement. The function rejects invalid tags, cyclic children, unsafe HTML props, unknown event keys, and invalid attribute values.
2. Choose the algorithm and data structures
The solution uses iterative depth-first traversal. A stack frame stores { vnode, node, childIndex }. This tells us which virtual node we are processing, which real DOM node belongs to it, and which child comes next. An active Set stores only element vnodes on the current ancestor path. That catches a real cycle but still allows the same vnode object to be reused later in a different non-cyclic branch. The explicit stack replaces recursion and avoids recursive stack overflow.
3. Apply props safely
className, style, and dataset use the specified DOM properties. Boolean attributes use presence or absence: true calls setAttribute(key, ''), while false leaves the attribute out. Ordinary attributes must contain strings and use setAttribute. Only onClick, onInput, and onChange are allowed event keys, and their values must be functions. innerHTML, dangerouslySetInnerHTML, and every other on... key are rejected. No string is evaluated as code.
4. Walk through the button example
The example vnode has type button, className save, disabled set to true, and one child string, Save. First, the tag name is validated. Then the code creates a <button> element. It sets className to save. Because disabled is true, it adds the disabled attribute. The next child is the string Save, so the code creates a text node and appends it. The button frame then finishes, leaves the active Set, and is popped.
5. Explain the stopping condition and result
A frame is finished when childIndex reaches children.length. At that point, its vnode is removed from active and the frame is popped. Processing continues until the stack is empty. The final returned node is the newly created <button class="save" disabled>Save</button> node. It has not been inserted into the document.
6. Explain why it is correct
Children are read from index 0 upward, so each parent’s DOM children are appended in the same order as the vnode children. Every string uses createTextNode, and every element uses createElement. The active Set contains exactly the element vnodes on the current path, so an object found there would create a cycle. Removing a vnode when its frame finishes allows safe reuse in a different branch.
7. Explain complexity and edge cases
Let n be the number of vnodes and p be the total number of property entries processed, including entries inside style and dataset. With average O(1) Set and Map operations, the expected time is O(n + p). Let h be the maximum element-tree depth. The explicit stack and active-ancestor Set both grow with the current path, so auxiliary space is O(h), excluding the returned DOM tree. Important cases are empty children, mixed text and element children, depth up to 10,000, invalid props or tag names, unknown event keys, and cyclic child references.
Key Insight / Why This Solution Works
The key idea is to simulate recursive depth-first rendering with an explicit stack. Each frame stores the current vnode, its real DOM node, and the next child index. The central invariant is that active contains exactly the element vnodes on the current ancestor path. Children are processed from index 0 upward and appended immediately, so DOM child order matches vnode child order. When a frame finishes, its vnode leaves active, so repeated vnode objects in separate non-cyclic branches are allowed. This explicit stack avoids recursive call-stack overflow.
Code
functionrenderVNode(vnode, documentRef = document) {
// Fixed supported Boolean attributes. Presence means true; absence means false.constBOOLEAN_ATTRS = newSet([
'allowfullscreen',
'async',
'autofocus',
'autoplay',
'checked',
'controls',
'default',
'defer',
'disabled',
'formnovalidate',
'hidden',
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nomodule',
'novalidate',
'open',
'playsinline',
'readonly',
'required',
'reversed',
'selected',
]);
// Only these event prop names are accepted, and their values must be functions.constEVENTS = newMap([
['onClick', 'click'],
['onInput', 'input'],
['onChange', 'change'],
]);
// Element types must be lowercase names made from letters, digits, and hyphens.constTAG_RE = /^[a-z][a-z0-9-]*$/;
functionvalidateElementVNode(value) {
// An element vnode must be a non-null object, not an array.if (!value || typeof value !== 'object' || Array.isArray(value)) {
thrownewTypeError('Invalid vnode');
}
// Validate the tag before calling createElement.if (typeof value.type !== 'string' || !TAG_RE.test(value.type)) {
thrownewTypeError('Invalid tag name');
}
// The constrained element shape requires an object props field and an array children field.if (!value.props || typeof value.props !== 'object' || Array.isArray(value.props)) {
thrownewTypeError('Invalid props');
}
if (!Array.isArray(value.children)) {
thrownewTypeError('Invalid children');
}
}
functionapplyProps(el, props) {
// Process every prop and send it only to an allowed DOM API.for (const [key, value] ofObject.entries(props)) {
// Raw HTML injection paths are always forbidden.if (key === 'innerHTML' || key === 'dangerouslySetInnerHTML') {
thrownewTypeError('Forbidden HTML prop');
}
// Allowed events must receive callable handlers. Strings are never evaluated.if (EVENTS.has(key)) {
if (typeof value !== 'function') {
thrownewTypeError('Event handler must be a function');
}
el.addEventListener(EVENTS.get(key), value);
continue;
}
// Reject every other event-looking prop.if (/^on/i.test(key)) {
thrownewTypeError('Unknown event key');
}
// className is one of the explicitly allowed DOM properties.if (key === 'className') {
if (typeof value !== 'string') {
thrownewTypeError('className must be a string');
}
el.className = value;
continue;
}
// style must be a plain object, and every style value must be a string.if (key === 'style') {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
thrownewTypeError('style must be a plain object');
}
for (const [name, styleValue] ofObject.entries(value)) {
if (typeof styleValue !== 'string') {
thrownewTypeError('style values must be strings');
}
el.style[name] = styleValue;
}
continue;
}
// dataset must be a plain object, and every dataset value must be a string.if (key === 'dataset') {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
thrownewTypeError('dataset must be a plain object');
}
for (const [name, dataValue] ofObject.entries(value)) {
if (typeof dataValue !== 'string') {
thrownewTypeError('dataset values must be strings');
}
el.dataset[name] = dataValue;
}
continue;
}
// A true Boolean attribute is present; false means it is omitted.if (BOOLEAN_ATTRS.has(key.toLowerCase())) {
if (typeof value !== 'boolean') {
thrownewTypeError('Boolean attribute must be boolean');
}
if (value) {
el.setAttribute(key, '');
}
continue;
}
// Ordinary attributes must have string values.if (typeof value !== 'string') {
thrownewTypeError('Attribute value must be a string');
}
el.setAttribute(key, value);
}
}
// A text vnode becomes a new text node immediately.if (typeof vnode === 'string') {
return documentRef.createTextNode(vnode);
}
// Validate the root before creating its DOM element.validateElementVNode(vnode);
// Create the detached root element and apply its props.const root = documentRef.createElement(vnode.type);
applyProps(root, vnode.props);
// active stores only vnodes on the current ancestor path.// This detects real cycles while allowing reuse in separate branches.const active = newSet([vnode]);
// Each frame tracks the vnode, its real node, and the next child to process.const stack = [{ vnode, node: root, childIndex: 0 }];
// Use iterative DFS so deep trees do not consume the JavaScript call stack.while (stack.length) {
const frame = stack[stack.length - 1];
// A finished frame leaves the active path and is removed from the stack.if (frame.childIndex >= frame.vnode.children.length) {
active.delete(frame.vnode);
stack.pop();
continue;
}
// Read children from left to right so append order is preserved.const child = frame.vnode.children[frame.childIndex++];
// Text children are created safely and appended immediately.if (typeof child === 'string') {
frame.node.appendChild(documentRef.createTextNode(child));
continue;
}
// Element children must satisfy the same vnode validation rules.validateElementVNode(child);
// A vnode already on the current ancestor path would create a cycle.if (active.has(child)) {
thrownewTypeError('Cyclic children');
}
// Create, configure, and append the child before entering its frame.const childNode = documentRef.createElement(child.type);
applyProps(childNode, child.props);
frame.node.appendChild(childNode);
// Enter the child path, then finish that subtree before the next sibling.
active.add(child);
stack.push({ vnode: child, node: childNode, childIndex: 0 });
}
// The complete root is returned without inserting it into the document.return root;
}
// Run the exact example from the diagram.const exampleVNode = {
type: 'button',
props: {
className: 'save',
disabled: true,
},
children: ['Save'],
};
const exampleNode = renderVNode(exampleVNode);
console.log(exampleNode.outerHTML);
console.log(exampleNode.disabled);
Time & Space Complexity
Let n be the number of vnodes and p be the total number of property entries processed, including entries inside style and dataset. With average O(1) Set and Map operations, the expected time is O(n + p). Let h be the maximum element-tree depth. The explicit stack and the active-ancestor Set contain only the current path, so auxiliary space is O(h), excluding the returned DOM tree. This avoids using the JavaScript call stack, which is why depth 10,000 can be supported.
Where it is used
This pattern is useful in UI renderers, template engines, test utilities, and small virtual-DOM systems that turn a safe tree description into real DOM nodes. The explicit-stack technique is also useful whenever a tree can be very deep and normal recursive traversal could overflow the JavaScript call stack.
Why Interviewers Ask This
This problem checks whether you understand DOM creation APIs, safe attribute and event handling, iterative tree traversal, cycle detection, and stack-depth limits in JavaScript. It also tests whether you can preserve child order while building a real tree from a virtual one. A strong answer clearly separates DOM properties from attributes, rejects unsafe HTML paths, uses functions for listeners, and explains the explicit stack and active-path invariant accurately.
Common interview mistakes
Using recursion, which can overflow the JavaScript call stack at depth 10,000.
Using one global visited Set, which wrongly rejects a vnode reused in a different non-cyclic branch.
Processing children in the wrong order or forgetting to remove a finished vnode from the active-ancestor Set.
Using innerHTML, accepting unknown on... event keys, or evaluating strings instead of requiring event-handler functions.
Treating ordinary attributes as arbitrary values instead of requiring strings, or handling false Boolean attributes as present instead of omitted.
Interview tip
Explain the active Set carefully. Say that it tracks only the current ancestor path, not every vnode ever seen. That one detail shows why real cycles are rejected while safe reuse in another branch still works.
Interviewer may ask next
How would the solution change if the same vnode object must never be reused anywhere in the tree, even in separate branches?
Keep the current active Set for cycle detection and add a second global seen Set. Add each element vnode to seen the first time it is accepted. If an element vnode is already in seen, reject it even when it is not on the current ancestor path. The traversal order and DOM construction stay the same. Expected time remains O(n + p) with average O(1) Set operations. Auxiliary space becomes O(n) because seen can hold every element vnode.
How would you support a new allowed event such as onKeyDown?
Add one entry to the event map, such as ['onKeyDown', 'keydown']. The existing event branch already requires the value to be a function and uses addEventListener, so the same safety rule still applies. Unknown on... keys remain rejected. The traversal does not change. Expected time remains O(n + p), and auxiliary traversal space remains O(h).
54. What is an algorithm?CodingEasy
i Question Details
Define an algorithm as a finite, unambiguous sequence of steps for transforming valid input into the required output. Explain correctness, termination, input constraints, boundary cases, time and space use, and how the same algorithm can be implemented in JavaScript using different data structures. Use a small search example before discussing the general idea.
Short Interview Answer (30-60 seconds)
An algorithm is a finite and clear sequence of steps that transforms valid input into the required output. In this example, I use linear search on [3, 7, 1, 9, 7, 4] to find 7. I check values from left to right and stop at the first match, which is index 1. The same steps work with a JavaScript Array or TypedArray. The search takes O(n) time and O(1) auxiliary space.
An algorithm is a clear set of steps for solving a problem. The steps must have one clear meaning and must eventually finish. Here, the input is [3, 7, 1, 9, 7, 4], and the target is 7. We want the index of the first matching value. Linear search fits because it checks each value from left to right and can stop when it finds the target. The same search steps can work with either a JavaScript Array or a TypedArray.
Useful Questions to Ask the Interviewer
Should I return the index of the first matching value?
What should I return when the target is not present?
Can the input be empty or contain duplicate values?
How to Explain It in an Interview
1. Understand the input and required output
The input is a sequence of values and a target value. In the diagram, the input is [3, 7, 1, 9, 7, 4], and the target is 7. The required output is the index of the first matching value. The answer is index 1 because the value at index 1 is 7. If no match exists, the function returns -1.
2. Choose linear search
Linear search checks the input from the beginning, one item at a time. It fits because the values are not shown as sorted. At each index, compare the current value with the target. If they are equal, return the current index immediately. Otherwise, move to the next item.
3. Walk through the example
Start at index 0. The current value is 3. Check whether 3 === 7. It is false, so move to the next index.
Now check index 1. The current value is 7. Check whether 7 === 7. It is true, so return index 1 and stop. Indices 2 through 5 are not processed because the answer has already been found.
4. Explain why the result is correct and why the algorithm finishes
Before checking index i, every earlier index has already been checked and did not contain the target. Therefore, the first successful comparison gives the first matching index. If the loop reaches the end without a match, then every valid index has been checked, so returning -1 is correct. The loop also always terminates because the index increases by one each time and cannot continue past the input length.
5. Explain the JavaScript data structures
The same linearSearch function works with a normal JavaScript Array and an Int32Array. Both provide indexed access and a length property, so the search steps do not change. For [3, 7, 1, 9, 7, 4] with target 7, both versions return index 1. Only the structure that stores the values changes.
6. Explain complexity and boundary cases
Let n be the number of input items. Linear search checks at most n items, so its time complexity is O(n). It uses only a fixed amount of extra state, so its auxiliary space complexity is O(1). Important boundary cases are an empty input, a single item, the target at the first or last position, duplicate target values, and a target that is not present.
Key Insight / Why This Solution Works
The key idea is to scan the input from left to right and return as soon as the target is found. The invariant is: before checking index i, every earlier index has already been checked and does not contain the target. This means the first successful comparison gives the first matching index. If no comparison succeeds, returning -1 is correct because every valid position was checked. The same algorithm works with both Array and Int32Array because both support indexed access and a length property.
Code
functionlinearSearch(arr, target) {
// Start at index 0 and move from left to right.for (let i = 0; i < arr.length; i++) {
// Compare the current value with the target.// Returning here stops at the first matching index.if (arr[i] === target) {
return i;
}
}
// If the loop ends, every item was checked and no match was found.return -1;
}
// Use the exact Array example from the diagram.const values = [3, 7, 1, 9, 7, 4];
const target = 7;
// The first 7 is at index 1.console.log(linearSearch(values, target)); // 1// Store the same values in a TypedArray.const data = newInt32Array([3, 7, 1, 9, 7, 4]);
// Run the same linear-search algorithm on the TypedArray.console.log(linearSearch(data, 7)); // 1
Time & Space Complexity
Let n be the number of items in the input. The algorithm checks at most n items, so the time complexity is O(n). It can stop earlier when it finds the target. The algorithm uses only a small fixed amount of extra state, mainly the loop index, so the auxiliary space complexity is O(1). The Array and TypedArray versions shown in the diagram use the same search steps and have the same complexity.
Where it is used
Linear search is useful when data is small, unsorted, or searched only occasionally. Frontend code can use it to find the first matching item in a simple in-memory list. The same pattern also works for indexed numeric data stored in a TypedArray.
Why Interviewers Ask This
This question checks whether the candidate understands what an algorithm is, not just how to write code. The interviewer can evaluate whether the candidate can describe clear finite steps, explain correctness and termination, respect input constraints, handle boundary cases, distinguish values from indices, reason about early return, compare data structures, and state time and auxiliary space correctly. It also tests whether the candidate can express the same algorithm clearly in JavaScript.
Common interview mistakes
A common mistake is returning the value 7 instead of its index 1. Another mistake is continuing after the first match even though the diagram returns the first matching index. Candidates may also forget to return -1 when the target is absent. With duplicate values, they should return the first match because the search moves from left to right. Another mistake is claiming O(1) time because one indexed access is constant time. The full search may still inspect up to n items, so the time complexity is O(n).
Interview tip
Walk through the first two positions aloud: index 0 contains 3, so continue; index 1 contains 7, so return 1 and stop. This makes the processing order, early return, correctness, and complexity easy to explain.
Interviewer may ask next
What changes if the target is not present in the input?
The algorithm does not change. It checks each item from left to right. If no value equals the target, the loop finishes and returns -1. Correctness is preserved because every valid index has been checked before returning -1. The time complexity is O(n), and the auxiliary space complexity remains O(1).
What changes if duplicate target values exist?
Nothing changes when the required result is the first matching index. Linear search visits indices in increasing order and returns immediately at the first match. For [3, 7, 1, 9, 7, 4] with target 7, it returns index 1 and does not process the later 7 at index 4. The worst-case time remains O(n), and the auxiliary space remains O(1).
55. Implement a reusable counter factory.CodingEasy
i Question Details
Implement makeCounter(initial = 0) in modern JavaScript. initial is a finite safe integer; return a zero-argument function that returns the current integer and then increments its own private state by one. Separate counters must not share state, and callers must not be able to mutate the internal value except by invoking the returned function. Do not use globals, class fields, timers, or external libraries. Throw TypeError for a non-number and RangeError for a non-safe integer. Example: const c = makeCounter(3); [c(), c(), c()] must produce [3, 4, 5]. Time per call must be O(1) and retained space O(1).
Short Interview Answer (30-60 seconds)
I would keep the counter value inside the factory so each returned function has its own private state. First, I validate that initial is a number and a safe integer. Then I store it in current. On each call, I save the current value, increment current by one, and return the saved value. This makes separate counters independent and prevents direct state mutation. Each call takes O(1) time, and each counter keeps O(1) retained space.
The task is to create a function that makes an independent counter. The caller gives a starting whole number, or 0 is used by default. The returned function takes no arguments. Each time it is called, it gives back its current number and then moves to the next number. Different counters must remember different values. Outside code must not be able to directly change the remembered number. Invalid input must throw the required error. For the given example, starting at 3 makes three calls return 3, 4, and 5.
Useful Questions to Ask the Interviewer
If the counter eventually moves beyond JavaScript's safe-integer range, should it continue using normal number behavior, or should that later state be rejected?
Are only the required error types important, or do you also want specific error messages?
How to Explain It in an Interview
1. Understand the input and output
makeCounter(initial = 0) receives one starting value. The starting value must be a JavaScript number and must pass Number.isSafeInteger. The function returns another function that takes no arguments. Each time that returned function runs, it returns the current stored value and then increases the stored value by one.
2. Use a closure for private state
The solution uses a closure. A closure means the returned function keeps access to variables created inside makeCounter even after makeCounter has finished. We store the private counter value in current. Outside code cannot directly access that local variable. Every call to makeCounter creates a different current, so separate counters do not share state.
3. Validate and initialize the state
First, check typeof initial !== 'number'. If that condition is true, throw TypeError. Next, check Number.isSafeInteger(initial). If that condition is false, throw RangeError. After validation, set current = initial. In the diagram example, current starts at 3.
4. Walk through the example
Create const c = makeCounter(3). The private state is now current = 3. On the first call, save 3, change current to 4, and return 3. On the second call, save 4, change current to 5, and return 4. On the third call, save 5, change current to 6, and return 5. Therefore [c(), c(), c()] produces [3, 4, 5]. If we called it again, it would return 6 and then change current to 7.
5. Explain why the result is correct
The main invariant is that before each call, current is exactly the value that the next call must return. The function saves that value before changing the state. It then increments current by one. This means every call returns the correct current value, and the following call sees the next integer. Each factory call creates its own closure, so different counters stay independent.
6. Explain the JavaScript implementation
The outer function validates the starting value and creates the private current variable. The returned zero-argument function reads current into value, increments current, and returns value. The code uses no globals, class fields, timers, or external libraries. Because current is local to the factory and only the returned function closes over it, callers cannot directly mutate it.
7. Explain complexity and edge cases
Each counter call performs a fixed amount of work: read one value, increment once, and return one value. That is O(1) time per call. Each counter keeps one private numeric state value, so retained space is O(1). A non-number input throws TypeError. A numeric value that is not a safe integer, such as 1.5, NaN, or Infinity, throws RangeError. Negative safe integers and zero are valid starting values.
Key Insight / Why This Solution Works
Use a closure to store one private variable named current. Validate the starting value before creating the counter. The central invariant is: before every invocation, current is the exact value that the next call must return. The returned function first copies current into a local variable, then increments current, then returns the saved value. Saving before incrementing is required because the contract says to return the current value first. Every call to makeCounter creates a new closure, so separate counters keep separate state.
Code
functionmakeCounter(initial = 0) {
// Reject values that are not JavaScript numbers.if (typeof initial !== 'number') {
thrownewTypeError('initial must be a number');
}
// The starting value must be an integer JavaScript can represent safely.if (!Number.isSafeInteger(initial)) {
thrownewRangeError('initial must be a safe integer');
}
// This private state belongs only to this counter instance.let current = initial;
// The returned function closes over current and remembers it between calls.returnfunction () {
// Save the value for this call before changing the private state.const value = current;
// Advance the private state so the next call gets the next integer.
current += 1;
// Return the value that belonged to this invocation.return value;
};
}
// Run the exact example shown in the diagram.const c = makeCounter(3);
console.log([c(), c(), c()]); // [3, 4, 5]
Time & Space Complexity
Each call to the returned counter does a fixed amount of work. It reads the current number, saves it, adds one to the private state, and returns the saved number. Therefore each call takes O(1) time. Each counter keeps only one private current value between calls. The amount of retained memory does not grow as the counter is used, so retained space per counter is O(1).
Where it is used
This closure pattern is useful when a small piece of changing state should belong to one function instance instead of being global. Examples include local sequence counters, component-specific counters, test helpers, and small stateful utilities where callers should use a controlled function instead of changing the stored value directly.
Why Interviewers Ask This
This problem checks whether you understand JavaScript closures and lexical scope, not just how to increment a number. The interviewer can see whether you can keep state private without globals or classes, create independent state for separate factory calls, validate JavaScript numeric input correctly, preserve the required return-before-increment order, and explain why both execution time and retained memory stay O(1). It also checks whether your code and explanation describe the same behavior.
Common interview mistakes
A common mistake is incrementing current before saving it. That would make a counter starting at 3 return 4 first. Another mistake is storing the counter value in a global variable, which would make different counters share state. Candidates may also throw the wrong error type by treating every invalid value the same. Another mistake is exposing current through an object or property, which would let callers mutate it directly. It is also incorrect to claim that retained space grows with the number of calls.
Interview tip
State the invariant before writing the returned function: current is always the value that the next call should return. Then implement the operation in the same order as the diagram: save current, increment the private state, and return the saved value.
Interviewer may ask next
Why do two counters created with makeCounter not share their values?
Each call to makeCounter creates a new execution environment with its own current variable. The returned function closes over that specific variable. For example, const a = makeCounter(3) and const b = makeCounter(10) keep different private states. Calling a() changes only the state captured by a. It does not change b. The work per call remains O(1), and each counter retains O(1) space.
How would you change the counter if every returned value also had to remain a safe integer?
The current problem validates only the starting value. If every later returned value also had to be safe, I would check Number.isSafeInteger(current) before returning it. If it is no longer safe, I would throw RangeError. The closure design would stay the same. The invariant would become: current is the next value to return only while it is a safe integer. Each call would still take O(1) time and each counter would still retain O(1) space. The tradeoff is one extra constant-time validation on every call.
56. Implement a function that reports how many arguments it received.CodingEasy
i Question Details
Implement numberOfArguments(...args) so the returned value is the exact count of arguments supplied at the call site, including explicit undefined, omitted-versus-present distinctions, and any extra arguments. The function accepts arbitrary JavaScript values and must not inspect parameter names, function .length, or the caller. Use no external helpers. Examples: numberOfArguments() returns 0; numberOfArguments(undefined, null, 3) returns 3. The operation should run in O(1) time with respect to argument inspection and must not mutate any argument.
Short Interview Answer (30-60 seconds)
I would use a rest parameter so the function receives every supplied argument in one array called args. JavaScript keeps explicit undefined values in that array, so the exact number of supplied arguments is args.length. I do not need to inspect the values, parameter names, function .length, or the caller. I simply return args.length. Reading the count takes O(1) time with respect to argument inspection and O(1) additional space beyond the required rest-parameter array.
The function needs to report how many values were actually supplied when it was called. A value still counts when that value is undefined or null. Extra arguments also count. The simple idea is to collect every supplied argument into one array and return that array's length. JavaScript rest parameters do exactly this. For numberOfArguments(undefined, null, 3), the collected array is [undefined, null, 3], so its length and the returned result are both 3.
Useful Questions to Ask the Interviewer
Should an explicit undefined value count as an argument? Yes, the question says it should.
Should the function accept any number and type of JavaScript values? Yes, including extra arguments.
How to Explain It in an Interview
1. Understand the input and required output
The input is any number of JavaScript values supplied at the call site. The output is one number: the exact count of those supplied arguments. Calling the function with no arguments returns 0. Passing undefined still adds one argument because that value was explicitly supplied.
2. Choose the JavaScript feature
Use a rest parameter written as ...args. JavaScript collects all supplied arguments into the args array. This keeps the important difference between an omitted argument and an explicit undefined argument. The central rule is simple: every supplied argument becomes one element in args.
3. Walk through the verified example
Call numberOfArguments(undefined, null, 3). Three arguments are supplied at the call site. Inside the function, ...args creates [undefined, null, 3]. The array has length 3. The function reads args.length and returns 3. No argument value is inspected or changed.
4. Explain why the result is correct
The rest parameter creates one array element for every argument supplied at the call site. Therefore, args.length is exactly the number of supplied arguments. Explicit undefined is still one array element. A completely omitted argument creates no element. Any extra arguments are also collected and counted.
5. Explain the JavaScript implementation
The function declaration uses ...args to collect all supplied values. There is no loop and no condition because the actual values do not matter. The function reads the length property of the collected array and immediately returns it. It does not inspect parameter names, function .length, the caller, or the contents of the arguments.
6. Explain complexity and edge cases
Reading args.length is O(1) with respect to argument inspection because the function does not scan the argument values. The diagram describes O(1) additional space beyond the rest-parameter array created by JavaScript. Important cases are zero arguments, explicit undefined, arbitrary JavaScript values, extra arguments, and not mutating any supplied argument.
Key Insight / Why This Solution Works
The key insight is that a JavaScript rest parameter already records exactly which arguments were supplied. The function declares ...args, so each supplied argument becomes one element of the args array. The invariant is that args.length equals the number of arguments supplied at the call site. Because the answer is already available as the array length, there is no need to inspect values or loop through them. Returning args.length directly gives the required count.
Code
functionnumberOfArguments(...args) {
// The rest parameter collects every argument supplied at the call site.// Explicit undefined values are included because they were actually supplied.// The array length is the exact number of received arguments.// Reading length does not inspect or mutate any argument value.return args.length;
}
// Run the same verified example shown in the diagram.const result = numberOfArguments(undefined, null, 3);
// The call supplied three arguments, so this prints 3.console.log(result);
Time & Space Complexity
Time is O(1) with respect to argument inspection. The function only reads the array's length property and does not loop through the argument values. The diagram describes O(1) additional space beyond the args array created by the required rest parameter. The function itself creates no other data structure that grows with the number of arguments.
Where it is used
This pattern is useful in JavaScript functions that accept a variable number of inputs. Examples include utility functions, logging helpers, wrappers, adapters, and APIs that need to know how many values were supplied. A rest parameter is useful when the function needs access to the actual supplied arguments or their exact count.
Why Interviewers Ask This
The interviewer is checking whether the candidate understands JavaScript rest parameters and the difference between declared parameters and arguments actually supplied at runtime. The question also tests whether the candidate notices that explicit undefined still counts as a supplied argument. A strong answer avoids unnecessary loops, does not misuse function .length, does not inspect the caller, preserves arbitrary values, and explains the O(1) argument-inspection cost accurately.
Common interview mistakes
A common mistake is using the function's .length property. That reports the number of declared parameters, not the number supplied at a particular call site. Another mistake is treating explicit undefined as if the argument were omitted. It must still count. Candidates may also loop through args even though only args.length is needed. Another mistake is inspecting the caller or parameter names, which the question forbids. The function also must not mutate any supplied argument.
Interview tip
State the key distinction early: omitted and explicit undefined are different at the call site. Then show that ...args preserves this distinction automatically, so returning args.length is enough.
Interviewer may ask next
What changes if the function must also return the received arguments?
The same rest parameter can be used. Instead of returning only args.length, the function could return an object such as { count: args.length, args }. The argument-count calculation is still O(1) with respect to inspection because no values need to be scanned. The main tradeoff is that the result now exposes the collected arguments instead of only the count.
Why not use numberOfArguments.length to get the argument count?
Function .length describes the function's declared parameter structure, not how many arguments a specific call supplied. For a function declared with only a rest parameter, numberOfArguments.length is 0 even when the caller supplies several values. The rest array reflects the actual call, so args.length correctly distinguishes zero supplied arguments from explicit undefined and from any number of extra arguments.
57. Implement an awaitable sleep utility.CodingEasy
i Question Details
Create sleep(ms, { signal } = {}) that returns a promise fulfilling with undefined after at least ms milliseconds. ms must be a finite number from 0 through 60,000. Use browser timers only. If signal is already aborted or becomes aborted before the timer fires, clear the timer and reject with the signal's reason when available; remove any abort listener after settlement. Reject invalid delays with TypeError or RangeError. Example: await sleep(0) must settle asynchronously, after the current synchronous stack. No global state or third-party package is allowed.
Short Interview Answer (30-60 seconds)
I would validate the delay first, then check whether the optional signal is already aborted. If it is valid, I return a Promise, start a browser setTimeout, and attach one abort listener. If the timer fires first, I remove the listener and resolve with undefined. If abort happens first, I clear the timer, remove the listener, and reject with the abort reason. The computational work is O(1), and the auxiliary space is O(1).
The goal is to create a small sleep function for browser JavaScript. It waits for a requested delay and then finishes with undefined. The delay must be a finite number from 0 through 60,000. The caller may also provide a cancellation signal. If cancellation has already happened, or happens before the wait finishes, the function rejects instead. This approach fits because a browser timer handles the delay, while one cancellation listener handles the optional early stop.
Useful Questions to Ask the Interviewer
Should values such as strings, NaN, Infinity, negative numbers, and values above 60,000 be rejected? Yes. The contract requires that behavior.
If the AbortSignal has a reason, should that exact reason be used for rejection? Yes. Otherwise, the diagram uses new DOMException("Aborted", "AbortError").
How to Explain It in an Interview
1. Validate the input
First, I check ms. It must be a JavaScript number and it must be finite. If either check fails, I throw TypeError. Then I check the allowed range. A value below 0 or above 60,000 throws RangeError. A delay of 0 is valid.
2. Handle an already-aborted signal
Before creating the timer, I check signal?.aborted. If it is already true, I return a rejected Promise immediately. I reject with signal.reason when it is available. Otherwise, I use new DOMException("Aborted", "AbortError").
3. Create the promise and start the timer
The diagram uses await sleep(100, { signal: controller.signal }), where controller.signal is not aborted. I create a new Promise and start setTimeout for 100 milliseconds. The Promise remains pending while the timer is waiting.
4. Attach the abort listener
After starting the timer, I define onAbort and attach it with { once: true }. If abort happens before the timer fires, onAbort clears the timer, removes the abort listener, and rejects with signal.reason or the AbortError fallback.
5. Finish the verified example
In the diagram example, no abort happens. After at least 100 milliseconds, the timer callback runs. It removes the abort listener and resolves the Promise with undefined. Therefore, the exact result is undefined after at least 100 milliseconds.
6. Explain why it is correct
While the Promise is pending, this call has one timer and at most one abort listener. The timer path resolves the Promise only after the requested delay. The abort path cancels the pending timer and rejects instead. Each path removes the listener as part of cleanup. A Promise can settle only once, so only one final result is observed.
7. Explain complexity and edge cases
The function does a fixed amount of computational work, so its computational complexity is O(1). Its auxiliary space is O(1). sleep(0) is valid and still settles asynchronously because setTimeout runs its callback after the current synchronous stack. Other important cases are an already-aborted signal, an abort before the timer fires, a negative delay, a delay above 60,000, and a non-finite delay.
Key Insight / Why This Solution Works
The key idea is to let one browser timer and one optional AbortSignal control the same Promise. First, validate ms and handle an already-aborted signal. Then create the Promise, start setTimeout, and attach one abort listener. The central invariant is that while the Promise is pending, the call owns at most one active timer and one abort listener. If the timer wins, remove the listener and resolve with undefined. If abort wins, clear the timer, remove the listener, and reject. Promise settlement guarantees that only one outcome becomes the result.
Code
functionsleep(ms, { signal } = {}) {
// Reject values that are not finite JavaScript numbers.if (typeof ms !== 'number' || !Number.isFinite(ms)) {
thrownewTypeError('ms must be a finite number');
}
// Accept only the required inclusive range: 0 through 60,000 ms.if (ms < 0 || ms > 60000) {
thrownewRangeError('ms must be between 0 and 60000');
}
// If cancellation already happened, do not create a timer.if (signal?.aborted) {
returnPromise.reject(signal.reason ?? newDOMException('Aborted', 'AbortError'));
}
returnnewPromise((resolve, reject) => {
// Start one browser timer. Even a delay of 0 settles asynchronously.const timer = setTimeout(() => {
// The timer won, so the abort listener is no longer needed.
signal?.removeEventListener('abort', onAbort);
resolve(undefined);
}, ms);
// If abort wins before the timer fires, cancel the timer and reject.constonAbort = () => {
clearTimeout(timer);
// Remove the listener as part of settlement cleanup.
signal?.removeEventListener('abort', onAbort);
// Prefer the signal's reason and use the diagram's fallback if needed.reject(signal.reason ?? newDOMException('Aborted', 'AbortError'));
};
// Attach at most one abort listener for this sleep call.
signal?.addEventListener('abort', onAbort, { once: true });
});
}
// Verified diagram example: the signal is not aborted.asyncfunctiondemo() {
const controller = newAbortController();
const result = awaitsleep(100, {
signal: controller.signal,
});
console.log(result); // undefined, after at least 100 ms
}
demo();
Time & Space Complexity
The computational work is O(1). The function performs a fixed number of checks, starts one timer, and optionally attaches one event listener. The real-world waiting time is at least ms milliseconds, but that waiting does not mean the JavaScript function is doing O(ms) computational work. The auxiliary space is O(1) because each call keeps only a fixed amount of extra state: one Promise, one timer handle, and at most one abort handler.
Where it is used
This pattern is useful when browser code needs an awaitable delay, such as spacing retry attempts, pacing animation steps, delaying UI work, or waiting between asynchronous operations. AbortSignal support is useful when the larger operation can be cancelled, for example when a user leaves a page, cancels an action, or stops a request that is still waiting.
Why Interviewers Ask This
This problem checks whether you understand Promises, browser timers, asynchronous scheduling, AbortSignal cancellation, input validation, and cleanup. It tests whether you know that setTimeout(0) is still asynchronous and whether you can handle an already-aborted signal correctly. It also shows whether you prevent unnecessary timer work after cancellation, remove event listeners after settlement, and describe the O(1) computational work and O(1) auxiliary space accurately.
Common interview mistakes
Common mistakes are checking only typeof ms === "number" and accidentally accepting NaN or Infinity, forgetting the inclusive 0 through 60,000 range, failing to check an already-aborted signal, rejecting on abort without clearing the pending timer, or forgetting to remove the abort listener during cleanup. Another mistake is resolving with some timer value instead of undefined. Candidates may also incorrectly make sleep(0) synchronous or omit the AbortError fallback used by the approved solution.
Interview tip
Describe the implementation as two competing settlement paths. The timer path removes the listener and resolves with undefined. The abort path clears the timer, removes the listener, and rejects. Explaining those two paths makes the cleanup and correctness easy to verify.
Interviewer may ask next
What changes if I call sleep(0)?
The algorithm does not change. Zero is inside the valid range, so the function creates the Promise, starts setTimeout with 0, and attaches the optional abort listener. The timer callback still cannot run during the current synchronous stack. When it later runs, it removes the listener and resolves with undefined. The computational work remains O(1), and the auxiliary space remains O(1).
What happens if the signal aborts before the timer fires?
The abort handler runs first. It calls clearTimeout(timer), removes the abort listener, and rejects the Promise with signal.reason when available. Otherwise, it uses new DOMException("Aborted", "AbortError"). The timer path does not become the observed result because the abort path has already settled the Promise. The computational work and auxiliary space both remain O(1).
58. Implement a cancellable timeout.CodingEasy
i Question Details
Write setCancellableTimeout(callback, delay, ...args). callback must be a function and delay a finite non-negative number. Schedule one browser timeout that invokes callback(...args) at most once, and return a zero-argument cancel() function. Calling cancel before execution prevents the callback; repeated cancellation and cancellation after execution are harmless. Preserve the ordinary function call receiver as undefined in strict mode. Example: after const cancel = setCancellableTimeout(log, 10, 'x'); cancel();, log must never run. Use only setTimeout and clearTimeout; do not mutate arguments.
Short Interview Answer (30-60 seconds)
I would validate the callback and delay first. Then I would schedule exactly one browser timeout and keep its handle plus a done flag in the closure. When the timeout fires, I check done, mark it true, and call callback(...args). The returned cancel function also checks done. If it is still false, it clears the timeout and marks the operation finished. This makes cancellation idempotent and keeps the callback at most once. Scheduling, cancellation, and wrapper execution are O(1) time with O(1) auxiliary space.
This function schedules one callback to run later. It receives the callback, a delay, and any extra values to pass to the callback. It must also return a cancel function. If cancel is called before the scheduled callback runs, the callback must never run. Calling cancel again must be harmless. Calling cancel after the callback already ran must also be harmless. The solution stores the timeout handle and a done flag so both the timeout path and cancel path know whether the operation is already finished.
Useful Questions to Ask the Interviewer
Should an invalid callback throw a TypeError and an invalid delay throw a RangeError?
Should a delay of 0 still use the normal asynchronous setTimeout behavior?
How to Explain It in an Interview
1. Validate the inputs
The callback must be a function. If it is not, throw a TypeError. The delay must be a finite number that is zero or greater. If it is not, throw a RangeError.
2. Schedule one timeout and create shared state
Call setTimeout exactly once and save its handle as timerId. The timeout is scheduled for the requested delay. Keep a Boolean flag named done. It starts as false. A false value means the operation has not finished or been cancelled yet.
3. Handle timeout execution
When the timeout callback gets its turn, first check done. If done is already true, return without invoking the user's callback. Otherwise set done to true and call callback(...args). The call uses the ordinary function-call form. In strict-mode receiver semantics, that means no custom receiver is supplied.
4. Return an idempotent cancel function
The function returns a zero-argument cancel function. cancel first checks done. If done is already true, it returns immediately. Otherwise it calls clearTimeout(timerId) and marks done as true. This prevents the pending timeout from invoking the callback. Repeated cancellation and cancellation after execution therefore have no additional effect.
5. Walk through the verified example
The diagram uses const cancel = setCancellableTimeout(log, 10, 'x'); cancel();. At 0 ms, timeout T1 is scheduled for 10 ms and done is false. In the execution trace, cancel is called at about 2 ms, before T1 fires. cancel clears T1 and changes done to true. At 10 ms, T1 would have fired, but it was cancelled. The callback does not run, so log('x') never executes.
6. Explain why the solution is correct
The central invariant is that done tells both paths whether this timeout operation is already finished. Only a path that sees done as false can take the finishing action. The timeout path marks done before calling user code. The cancel path clears the pending timeout and marks done. Therefore the callback is invoked at most once, and cancellation is harmless when repeated or performed after execution.
7. Explain complexity and edge cases
Scheduling is O(1). Cancellation is O(1). The wrapper work when the timeout fires is O(1), excluding work performed inside the user's callback. The closure keeps only the timeout handle and one Boolean flag, so auxiliary space is O(1). Relevant edge cases are delay 0, repeated cancellation, cancellation after execution, a non-function callback, and a non-finite or negative delay.
Key Insight / Why This Solution Works
The key idea is to share one closed-over state flag between the scheduled timeout and the returned cancel function. The invariant is: when done is false, the operation is still pending and the callback may run; when done is true, the operation is finished and this timeout must not start the callback. A single timerId identifies the one scheduled timeout. The timeout path checks done, marks it true, then invokes callback(...args). The cancel path checks the same flag, clears timerId, and marks done true. Because both paths use the same finished state, cancellation is idempotent and callback execution is limited to at most once.
Code
functionsetCancellableTimeout(callback, delay, ...args) {
// The callback must be callable.if (typeof callback !== 'function') {
thrownewTypeError('callback must be a function');
}
// The delay must be a finite number that is zero or greater.if (!Number.isFinite(delay) || delay < 0) {
thrownewRangeError('delay must be a finite non-negative number');
}
// Schedule exactly one browser timeout and keep its handle for cancel().let timerId = setTimeout(function () {
// A finished or cancelled operation must not invoke the callback.if (done) return;
// Mark the operation finished before running user code.
done = true;
// Use an ordinary function call and pass the original extra arguments.callback(...args);
}, delay);
// Shared state for the timeout path and the cancellation path.// Even delay = 0 runs in a later task, so this is initialized first// before the scheduled callback can execute.let done = false;
// Return the required zero-argument, idempotent cancellation function.returnfunctioncancel() {
// Repeated cancellation and cancellation after execution are harmless.if (done) return;
// Clear the one pending timeout before it can invoke the callback.clearTimeout(timerId);
// Record that this timeout operation is finished.
done = true;
};
}
// Verified example from the diagram.functionlog(value) {
'use strict';
console.log(value);
}
const cancel = setCancellableTimeout(log, 10, 'x');
// Cancel before the 10 ms timeout executes, so log('x') never runs.cancel();
Time & Space Complexity
Creating the timeout takes O(1) time. Calling cancel takes O(1) time. When the timeout fires, the wrapper itself also performs O(1) work before calling the user's callback. Work done inside the user's callback is separate. The function keeps only one timeout handle and one Boolean flag in its closure, so auxiliary space is O(1).
Where it is used
This pattern is useful when work should happen later but may become unnecessary before its scheduled time. Examples include delayed UI updates, debounced cleanup steps, scheduled notifications, pending retries, and timers that should be cancelled when a view or component no longer needs them.
Why Interviewers Ask This
This problem checks whether you understand browser timers, closures, input validation, and small state transitions in JavaScript. It also tests whether you can make an operation safely cancellable and idempotent. The interviewer wants to see that you schedule only one timeout, reason about the timeout and cancel paths consistently, preserve ordinary callback invocation semantics, pass extra arguments correctly, and explain why the callback can run at most once with accurate O(1) time and space costs.
Common interview mistakes
A common mistake is returning the timeout handle instead of the required zero-argument cancel function. Another is scheduling more than one timeout. Candidates may forget to make repeated cancel calls harmless or forget that cancel after execution must also be harmless. Input validation is another common source of errors, especially accepting Infinity, NaN, or a negative delay. It is also important to mark done before invoking user code and to call callback(...args) normally rather than supplying a custom receiver.
Interview tip
Explain the done flag as the shared invariant first. Say that the timeout path and cancel path both consult the same finished state. Then walk through T1: schedule it for 10 ms, cancel it before 10 ms, clear T1, set done to true, and show that log('x') never runs.
Interviewer may ask next
What happens if cancel() is called after the callback has already executed?
The timeout path sets done to true before invoking the callback. A later cancel call checks done, sees true, and returns immediately. It does not invoke the callback again and does not need to clear the already-finished timer. The operation remains harmless and idempotent. Time is O(1), and auxiliary space remains O(1).
Why keep the done flag if clearTimeout can cancel a pending timeout?
clearTimeout handles the pending timer, but done gives both paths one shared finished state. It lets repeated cancel calls return safely and lets cancel after successful execution do nothing. It also makes the at-most-once rule explicit because the timeout checks the same flag before invoking the callback. The extra cost is one Boolean value, so time remains O(1) and auxiliary space remains O(1).
59. Convert an error-first callback API into a promise API.CodingMedium
i Question Details
Implement promisify(fn). The returned function forwards its this and arguments, appends a callback of shape (error, value), and returns a promise. Reject when error is not nullish; otherwise fulfill with value. Ignore repeated callback invocations after the first settlement, and convert a synchronous throw from fn into rejection. Do not rely on host-specific globals or multiple success values. Example: promisifying (x, cb) => setTimeout(() => cb(null, x * 2), 0) and calling it with 4 must fulfill with 8. Reject a non-function and avoid retaining arguments after settlement.
Short Interview Answer (30-60 seconds)
I would wrap the callback API in a function that returns a Promise. The wrapper forwards the original this value and arguments, then appends one error-first callback. I use a called flag so only the first settlement attempt matters. A non-nullish error rejects the Promise. Otherwise, the value fulfills it. I also convert synchronous throws into rejection and clear stored argument and context references after settlement. The control logic uses O(1) extra state besides the forwarded arguments.
The goal is to turn a function that reports its result through a callback into one that returns a Promise. The new function must pass along the same this value and arguments. It adds one callback that receives an error and one value. The first settlement attempt decides the result. A real error rejects the Promise. A null or undefined error fulfills it. A synchronous throw also becomes a rejection. After settlement, the wrapper clears its stored argument and context references. In the given example, calling the promisified function with 4 must fulfill with 8.
Useful Questions to Ask the Interviewer
Should a non-function produce a rejected Promise when the returned wrapper is called?
Should only the first callback invocation affect the Promise?
Should both null and undefined mean that no error occurred?
How to Explain It in an Interview
1. Understand the input and required output
The input to promisify is fn, an error-first callback function. promisify returns another function. When that returned function is called, it must return a Promise. It forwards the same this value and original arguments to fn. It also appends a callback with the shape (error, value).
If error is not nullish, the Promise rejects with that error. Nullish means null or undefined. Otherwise, the Promise fulfills with value. Only the first settlement attempt is allowed to affect the Promise.
2. Create the wrapper and Promise
The returned wrapper receives ...args and saves its current this value in context. It then creates and returns a Promise.
Inside the Promise, the code checks whether fn is a function. If it is not, the wrapper clears args and context, rejects the Promise with TypeError, and returns.
For a valid function, called starts as false. This flag tells us whether the Promise has already been settled by the callback or by a caught synchronous throw.
3. Handle the callback exactly once
The appended callback receives error and value. It first checks called. If called is already true, it returns immediately. This ignores repeated callback calls.
On the first callback call, it changes called to true. It then clears args and context so those wrapper-held references do not remain after settlement.
Next, it checks error. If error != null, it rejects with error. Otherwise, it resolves with value.
4. Call the original function and handle synchronous throws
The wrapper invokes the original function with fn.apply(context, [...args, callback]). This forwards the captured this value. It forwards every original argument and places the new callback last.
The call is inside try/catch. If fn throws before anything has settled, the catch block sets called to true, clears args and context, and rejects with the thrown error. If a synchronous callback already settled first and fn then throws, the catch block sees that called is true and returns without changing the Promise result.
5. Walk through the verified example
The example function is (x, cb) => setTimeout(() => cb(null, x * 2), 0). We create doubleAsyncP = promisify(doubleAsync), then call doubleAsyncP(4).
The wrapper captures its this value and the argument 4. It creates a Promise and calls doubleAsync with 4 and the appended callback. Later, doubleAsync calls that callback with null and 8.
called is false, so this is the first settlement. The wrapper sets called to true, clears args and context, sees that error is null, and resolves with 8. The returned Promise therefore fulfills with 8.
6. Explain why the solution is correct
The central invariant is that called is false before the first settlement and true after it. Every later callback call or caught synchronous throw checks this state before trying to settle again. Therefore, only the first settlement attempt can affect the result.
The error test also matches the required contract. Null and undefined follow the success path. Any other error value follows the rejection path.
7. Explain complexity and edge cases
The wrapper uses a fixed amount of bookkeeping state: context, called, and the callback. The diagram describes this control overhead as O(1) extra state besides the forwarded arguments. Forwarding a arguments requires handling those a supplied values, and the spread used for invocation materializes an argument list proportional to a.
Important edge cases are a non-function fn, repeated callback calls, null or undefined errors, a synchronous throw, and a throw that happens after a synchronous callback already settled the Promise.
Key Insight / Why This Solution Works
The key idea is to place the callback-style API behind a Promise wrapper and use one boolean, called, as the settlement invariant. Before settlement, called is false. The first callback or synchronous throw changes the state to settled. Every later settlement attempt returns without changing the result. The callback rejects when error != null and otherwise resolves with value. apply preserves the original this value, the original arguments are forwarded, and the callback is appended last. The wrapper clears its stored args and context references when the operation settles.
Code
functionpromisify(fn) {
// Return a wrapper that captures the caller's arguments and this value.returnfunction (...args) {
let context = this;
// Every call to the wrapper returns a Promise.returnnewPromise((resolve, reject) => {
// Report an invalid fn through the returned Promise.if (typeof fn !== 'function') {
// Release wrapper-held references before rejecting.
args = null;
context = null;
reject(newTypeError('promisify(fn): fn must be a function'));
return;
}
// This flag allows only the first settlement attempt to have an effect.let called = false;
functioncallback(error, value) {
// Ignore callback calls after the first settlement.if (called) return;
// Mark the operation settled before resolving or rejecting.
called = true;
// Release the stored arguments and context after settlement.
args = null;
context = null;
// Null and undefined mean success. Any other error means failure.if (error != null) {
reject(error);
} else {
resolve(value);
}
}
try {
// Preserve this, forward the original arguments, and append callback.
fn.apply(context, [...args, callback]);
} catch (error) {
// A callback may have settled synchronously before fn threw.if (called) return;
// Otherwise, this synchronous throw is the first settlement.
called = true;
// Release wrapper-held references on the rejection path.
args = null;
context = null;
reject(error);
}
});
};
}
// Verified example from the diagram.constdoubleAsync = (x, cb) => {
setTimeout(() =>cb(null, x * 2), 0);
};
// Convert the callback API into a Promise API.const doubleAsyncP = promisify(doubleAsync);
// Calling with 4 fulfills with 8.doubleAsyncP(4)
.then((value) =>console.log(value))
.catch((error) =>console.error(error));
Time & Space Complexity
The diagram shows O(1) wrapper control overhead and O(1) bookkeeping state besides the forwarded arguments. The wrapper itself keeps only a fixed number of control values such as context and called. If a is the number of supplied arguments, JavaScript must also represent those a arguments, and [...args, callback] creates an invocation array proportional to a. So the fixed control work is O(1), while materializing and forwarding the argument list takes O(a) time and temporary O(a) storage. After settlement, the wrapper clears its stored args and context references.
Where it is used
This pattern is useful when older browser code or a library exposes an error-first callback API but newer application code uses Promises or async/await. The wrapper creates a Promise-friendly boundary without changing the original function. It is useful during gradual migrations from callback-based asynchronous code to Promise-based code.
Why Interviewers Ask This
This question checks whether you understand callback and Promise control flow. It tests whether you preserve this and arguments, interpret nullish errors correctly, convert synchronous exceptions into Promise rejection, and stop repeated callbacks from changing the result. It also tests careful reasoning about settlement order and closure-held references. The interviewer can also see whether you write browser-side JavaScript precisely and explain the difference between the fixed control state and the supplied argument storage.
Common interview mistakes
One mistake is allowing every callback invocation to try to settle the Promise instead of guarding settlement with called. Another is checking only error === null, which incorrectly treats undefined as failure. Candidates may also lose the original this value by calling fn without the saved context. Another mistake is forgetting to catch a synchronous throw from fn. It is also easy to forget the case where the callback settles synchronously and fn throws afterward. Finally, the wrapper should clear its stored args and context references after settlement.
Interview tip
Explain the called flag as the single-settlement invariant before writing the callback. Then show that both the callback path and the synchronous-throw path obey that same flag. This makes the repeated-callback case and the callback-then-throw case easy to justify.
Interviewer may ask next
What would change if the callback could return multiple success values?
The current contract intentionally accepts only one success value. If the API instead used callback(error, ...values), I would change the wrapper contract so the callback gathers those values and resolves with one container such as an array. The called guard, nullish-error check, this forwarding, synchronous-throw handling, and cleanup logic would stay the same. Collecting k success values would require O(k) result space.
What happens if fn calls the callback successfully and then throws synchronously?
The callback runs first, sees called is false, sets it to true, clears args and context, and resolves the Promise. If fn then throws before returning, the catch block receives that error. The catch checks called and immediately returns because settlement already happened. The later throw therefore does not replace the successful result. This keeps the first-settlement rule intact.
60. Flatten a nested object into dot-delimited paths.CodingMedium
i Question Details
Write squashObject(input). Accept a plain object or array containing JSON-compatible primitives, plain objects, and arrays; cycles, symbols, functions, bigint, and keys containing . are invalid. Return a null-prototype object whose keys are dot-delimited paths. Preserve empty objects and arrays by assigning them at their own path, and represent a root primitive only if you explicitly define a root key; for this task the root must be a container. Array indexes are decimal segments. Example: {a:{b:1}, c:[2,{d:3}]} becomes {'a.b':1,'c.0':2,'c.1.d':3}. Do not mutate input and detect cycles.
Short Interview Answer (30-60 seconds)
I would use depth-first search to walk through the nested object or array. Each recursive call carries the current dot-delimited path. When I reach a valid primitive, I store it in a null-prototype result object. Empty objects and arrays are stored at their own path. A WeakSet tracks containers on the active recursion path so cycles are detected. I also reject dotted keys and unsupported values. The traversal is O(N) time, with O(D) recursion depth and O(N) storage including the result.
The input is one plain object or array that can contain simple values, more plain objects, and arrays. The goal is to replace the nested shape with path-value entries. Object keys become path parts. Array positions become decimal path parts. Empty containers must still appear in the result. The original input must not be changed. The solution walks through the structure with depth-first search and builds the correct path while it moves deeper.
Useful Questions to Ask the Interviewer
Should empty objects and arrays be kept in the flattened result? Yes, the question requires this.
Should array indexes be written as decimal path segments such as "c.0" and "c.1"? Yes.
Should cycles, unsupported values, invalid container types, and object keys containing "." cause an error? Yes.
How to Explain It in an Interview
1. Understand the input and required output
The root must be a plain object or an array. Valid leaf values are null, strings, numbers, and booleans. Plain objects and arrays can appear at any depth. Symbols, functions, bigint values, cycles, and object keys containing "." are invalid. Other object types such as Date, Map, Set, and class instances are also rejected because they are not plain objects. The function returns an object created with Object.create(null). Its keys are paths such as "a.b" and "c.1.d".
2. Choose DFS and track the current path
I use recursive depth-first search. Each recursive call receives the current value and the path to that value. If the value is a valid primitive, I write it to the result. If it is an array or plain object, I visit its children. The main invariant is simple: every helper call receives the exact path of its current value from the root.
A WeakSet named seen tracks only containers on the active recursion path. Before entering a container, I check whether it is already in seen. If it is, there is a cycle. After finishing that container, I remove it from seen. This backtracking means the same object may appear again through a different non-cyclic branch without being mistaken for a cycle.
3. Initialize the state
The result starts as Object.create(null). This creates an object with no Object.prototype inheritance. The seen WeakSet starts empty. The first recursive call is helper(input, ""), so traversal begins at the root with an empty path.
4. Walk through the verified example
The example input is {a:{b:1}, c:[2,{d:3}]}.
Step 1 starts at the root. The root is a non-empty plain object, so DFS visits its keys.
Step 2 visits a at path "a". Its value is {b:1}. This object is not empty, so DFS continues inside it.
Step 3 visits b with value 1 at path "a.b". The value is a primitive, so the algorithm stores result["a.b"] = 1. The result is now {"a.b":1}.
Step 4 returns to the root and visits c at path "c". Its value is [2,{d:3}]. The array is not empty, so DFS visits its indexes.
Step 5 visits index 0. The value is 2 and the path is "c.0". The algorithm stores result["c.0"] = 2. The result is now {"a.b":1,"c.0":2}.
Step 6 visits index 1. The value is {d:3} and the path is "c.1". It is a non-empty plain object, so DFS continues inside it.
Step 7 visits d with value 3 at path "c.1.d". The algorithm stores result["c.1.d"] = 3. The final result is {"a.b":1,"c.0":2,"c.1.d":3}.
5. Explain why the result is correct
Every recursive call carries the exact location of its value. Object keys add one key segment. Array positions add one decimal index segment. Therefore, every primitive is stored under the path that identifies its original position. Empty objects and arrays are written at their own path instead of disappearing. The WeakSet prevents infinite recursion because revisiting a container that is still on the active DFS path means a cycle exists.
6. Explain the JavaScript implementation
The function first checks that the root is an array or plain object. It creates the null-prototype result and the WeakSet. The helper rejects symbol, function, and bigint values before doing anything else. It stores valid primitives immediately. Any remaining value must be an array or plain object. The helper checks for a cycle before descending. Arrays are processed from index 0 upward. Plain objects are processed with Object.keys. Every object key is checked for a dot before recursion. Empty containers are stored directly at their current path. After a container is finished, it is removed from seen.
7. Explain complexity and edge cases
The diagram gives O(N) time, where N is the total number of visited objects, arrays, and primitive values. The recursion depth is O(D), where D is the maximum nesting depth. The returned result and cycle-tracking state can grow with the input, so the diagram summarizes storage as O(N), plus the O(D) recursion stack. Important cases are empty objects, empty arrays, deep nesting, dotted keys, cycles, unsupported primitive types, and non-plain object containers.
Key Insight / Why This Solution Works
Use recursive depth-first search. The helper receives the current value and its dot-delimited path. A valid primitive becomes one result entry. An empty object or array also becomes one result entry at its current path. For a non-empty array, recurse through indexes in order. For a non-empty plain object, recurse through Object.keys in order after rejecting keys containing a dot. A WeakSet tracks containers on the active recursion path. The central invariant is that every helper call receives the exact path of its value in the original input. Removing a container from the WeakSet after processing its children restores the path-local state.
Code
functionsquashObject(input) {
// The root must be one of the two supported container types.if (!isValidContainer(input)) {
thrownewTypeError('Root must be a plain object or array');
}
// A null-prototype object avoids inherited Object.prototype properties.const result = Object.create(null);
// Track containers on the active DFS path so cycles can be detected.const seen = newWeakSet();
functionhelper(value, path) {
const type = typeof value;
// These value types are explicitly invalid for this problem.if (type === 'symbol' || type === 'function' || type === 'bigint') {
thrownewTypeError('Unsupported value type at path: ' + path);
}
// A valid primitive is a leaf, so record it at its exact dot path.if (isPrimitive(value)) {
result[path] = value;
return;
}
// Every remaining value must be an array or a plain object.if (!isValidContainer(value)) {
thrownewTypeError('Invalid container at path: ' + path);
}
// Reaching the same active container again means there is a cycle.if (seen.has(value)) {
thrownewTypeError('Cycle detected');
}
seen.add(value);
if (Array.isArray(value)) {
// Empty arrays must remain visible in the flattened result.if (value.length === 0) {
result[path] = [];
} else {
// Array indexes become decimal path segments such as c.0 and c.1.for (let index = 0; index < value.length; index++) {
const nextPath = path ? path + '.' + index : String(index);
helper(value[index], nextPath);
}
}
} else {
const keys = Object.keys(value);
// Empty plain objects must remain visible in the flattened result.if (keys.length === 0) {
result[path] = {};
} else {
for (const key of keys) {
// A source key containing a dot would make the flattened path ambiguous.if (!isValidKey(key)) {
thrownewTypeError('Invalid key: ' + key);
}
// Extend the current path with this object key and recurse.const nextPath = path ? path + '.' + key : key;
helper(value[key], nextPath);
}
}
}
// Backtrack so seen contains only containers on the current DFS path.
seen.delete(value);
}
// Start traversal at the root, which has no path segment of its own.helper(input, '');
return result;
}
functionisPlainObject(value) {
// Only ordinary objects or null-prototype objects count as plain objects.if (value === null || typeof value !== 'object') {
returnfalse;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
functionisValidContainer(value) {
// Arrays and plain objects are the only supported container types.returnArray.isArray(value) || isPlainObject(value);
}
functionisPrimitive(value) {
// These are the JSON-compatible primitive categories used by the diagram.const type = typeof value;
return value === null || type === 'string' || type === 'number' || type === 'boolean';
}
functionisValidKey(key) {
// The problem rejects object keys that contain a dot.returntypeof key === 'string' && !key.includes('.');
}
// Run the same verified example shown in the diagram.const input = { a: { b: 1 }, c: [2, { d: 3 }] };
const output = squashObject(input);
console.log(output);
// Null-prototype object containing:// { 'a.b': 1, 'c.0': 2, 'c.1.d': 3 }
Time & Space Complexity
The diagram gives O(N) time, where N is the total number of visited objects, arrays, and primitive values. Each reachable value is handled once by the DFS traversal. The maximum recursion depth is O(D), where D is the deepest nesting level. The flattened result grows with the input, and the cycle-detection state also uses memory. Following the diagram, total stored output and traversal bookkeeping are O(N), with an additional O(D) recursion stack.
Where it is used
This pattern is useful when nested configuration data, form data, application state, or JSON-like data must be turned into path-value entries. Dot paths can make nested values easier to index, compare, display, or send to systems that work with flat key-value records. The same DFS pattern is also useful whenever JavaScript code must walk nested data while remembering the path to each value.
Why Interviewers Ask This
This problem checks whether a candidate can traverse recursive JavaScript data without losing structural information. It tests path construction, arrays versus plain objects, recursion state, cycle detection, input validation, and non-mutation. It also shows whether the candidate notices less obvious requirements such as preserving empty containers and returning a null-prototype object. A strong answer keeps the code, traversal order, edge cases, and complexity explanation consistent.
Common interview mistakes
A common mistake is to recurse into every JavaScript object. That would incorrectly accept Date, Map, Set, or class instances instead of only arrays and plain objects. Another mistake is forgetting to preserve empty objects and arrays, which makes them disappear from the output. Candidates may also forget to reject object keys containing a dot, build array paths incorrectly, or mutate the original input. Cycle detection must be path-local. If a container is never removed from seen after recursion, a shared reference can be incorrectly reported as a cycle.
Interview tip
Explain the invariant before writing the recursion: every helper call receives the exact flattened path of its current value. Then show how one object key or array index extends that path. This makes the primitive case, empty-container case, cycle check, and final output much easier to explain.
Interviewer may ask next
How would you handle a very deeply nested input that may exceed the JavaScript call stack?
I would keep the same depth-first traversal rules but replace recursive calls with an explicit stack. Each stack entry would store the current value, its path, and the traversal state needed to preserve the same child order and path-local cycle tracking. This avoids depending on the JavaScript call stack. The traversal remains O(N) time under the diagram's model. Extra memory is O(N) in the worst case for the explicit traversal state, result, and cycle bookkeeping. The tradeoff is more implementation complexity.
What happens if the same object is referenced from two different branches but there is no cycle?
The shown solution allows that case. The WeakSet represents only the active recursion path. A container is added before its children are processed and removed when that recursive call finishes. If the same object is reached later through another completed branch, it is no longer in seen, so it can be processed again under the new path. If it is reached while still active, that is a real cycle and the function throws. The traversal and memory bounds stay consistent with the diagram.
More questions load as you scroll
Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.