277 JavaScript Frontend Developer Interview Questions & Answers

133 top • 30 Amazon • 15 Apple • 29 Google • 18 Meta • 21 Microsoft • 20 Netflix • 11 NVIDIA

JavaScript Frontend Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

41. How do bound functions behave with arguments, `this`, and construction?Language SpecificMedium

Question Details

Bind a function with a receiver and one leading argument, then call it normally and with new. Explain partial application, the ignored bound receiver during construction, the resulting instance's prototype relationship, length and name changes at a high level, and why a bound function has no useful own prototype property.

Short Interview Answer (30-60 seconds)

A bound function remembers leading arguments and normally uses its bound this value. The bound arguments come first, before arguments supplied later. If I call the bound function with new, JavaScript ignores the bound receiver and creates a fresh this for construction. The bound arguments still apply. Construction is forwarded to the original target, so in the normal case the new instance inherits from the target prototype. A bound function also gets an adjusted length and name, and it normally has no own prototype property.

Detailed Explanation

See the Code while reading this explanation.

A bound function is a new function that remembers another function, a value for this, and any arguments supplied to bind. During a normal call, JavaScript uses the remembered this value and puts the remembered arguments before later arguments. Construction works differently. If the bound function is called with new, JavaScript creates a fresh value for this and ignores the remembered receiver. The remembered arguments still come first. Construction is forwarded to the original function. JavaScript also gives the bound function its own reported argument count and name, but normally no own prototype property.

Useful Questions to Ask the Interviewer
  1. Should I show both a normal call and a constructor call with the same bound function?
  2. Do you want me to cover length, name, instanceof, and the missing own prototype property?
How do bound functions behave with arguments, `this`, and construction? diagram
How to Explain It in an Interview

Suppose Person receives role and name. If I run Person.bind(savedReceiver, "Engineer"), JavaScript creates a bound function. It remembers savedReceiver and the leading argument "Engineer".

For a normal call such as BoundPerson("Maya"), the original Person function receives "Engineer" first and "Maya" second. Its this value is savedReceiver. Trying to call that bound function with another receiver through call or apply does not replace the original bound receiver.

Construction is different. With new BoundPerson("Leo"), JavaScript forwards construction to Person. The saved receiver is ignored because construction needs a fresh this. The saved argument remains, so Person receives "Engineer" and "Leo". In this example, Person does not return a replacement object, so the result inherits from Person.prototype. It is an instance of Person. It also passes instanceof BoundPerson because the bound function delegates that check to its target.

A bound function normally has no own prototype property. Therefore BoundPerson.prototype is undefined, even though new BoundPerson() works when Person itself can be constructed. This also means a bound function cannot normally be used directly as the parent in a class declaration.

Its length is generally the target length reduced by the number of bound arguments, with zero as the lower limit. Its name is formed from the target name with "bound " in front. Each call to bind creates a new function object, so repeated binding creates extra allocations and can cause callback identity problems.

Example

The example binds Person to savedReceiver and also binds the first argument, "Engineer". A normal call uses savedReceiver as this and places the bound argument before the later argument. A constructor call with new ignores savedReceiver, creates a fresh instance, and still places "Engineer" before "Leo". Because Person does not return a replacement object, the created result inherits from Person.prototype. The example also shows that both instanceof Person and instanceof BoundPerson succeed, that length is reduced, that name gains the "bound " prefix, and that the bound function has no own prototype property.

Code
function Person(role, name) {
  // Save both arguments on the object currently used as this.
  this.role = role;
  this.name = name;
}

const savedReceiver = {};

// Bind the receiver and prefill the first argument.
const BoundPerson = Person.bind(savedReceiver, 'Engineer');

// A normal call uses the bound receiver and places the bound argument first.
BoundPerson('Maya');
console.log(savedReceiver); // { role: "Engineer", name: "Maya" }

// Construction ignores the bound receiver but keeps the bound argument.
const person = new BoundPerson('Leo');
console.log(person.role, person.name); // Engineer Leo

// Person does not return a replacement object, so the result uses Person.prototype.
console.log(Object.getPrototypeOf(person) === Person.prototype); // true

// The instance check works for both the target and its bound function.
console.log(person instanceof Person); // true
console.log(person instanceof BoundPerson); // true

// One leading argument was bound, so the reported remaining parameter count is one.
console.log(BoundPerson.length); // 1

// The bound function name contains the target name with the bound prefix.
console.log(BoundPerson.name); // bound Person

// A bound function normally has no own prototype property.
console.log(Object.hasOwn(BoundPerson, 'prototype')); // false
console.log(BoundPerson.prototype); // undefined
Where it is used

In frontend code, bind is useful when a callback must keep a specific receiver or when some leading arguments should be filled in ahead of time. A common example is passing an object method to another API while keeping that object as this. Bind can also create small partially applied callbacks. In production code, keep the bound function when its identity matters. Creating a fresh bound function for event registration and then creating another one for event removal will not match the original callback. Repeated binding also creates additional function objects, so stable callbacks are usually clearer when the same function is reused many times.

Why Interviewers Ask This

Interviewers ask this to check whether you understand that bind affects normal calls and constructor calls in different ways. They want to see whether you understand bound arguments, bound this, construction, prototype lookup, function metadata, function identity, and practical callback use.

Common interview mistakes

A common mistake is thinking the bound receiver is always used. It is ignored when the bound function is constructed with new. Another mistake is thinking bound arguments disappear during construction. They still come first. Some candidates expect BoundPerson.prototype to equal Person.prototype, but the bound function normally has no own prototype property. Another mistake is assuming the constructor result must always inherit from the target prototype. A constructor can explicitly return another object, and that object can become the result instead. Candidates also sometimes call bind again and expect the second receiver to replace the first one. Rebinding cannot replace the original bound this, although it can add more bound arguments.

Interview tip

Start with the contrast between a normal call and a call with new. Say that bound arguments stay in both cases, while the bound receiver is ignored during construction. Then explain the target prototype relationship, the constructor return edge case, instanceof, length, name, and the missing own prototype property.

Interviewer may ask next
What happens if the original target cannot be used as a constructor and I call its bound function with `new`?

It throws a TypeError. Binding does not make a target constructible. For example, an arrow function cannot be constructed, so a bound version of that arrow function also cannot be constructed. This matters because bind preserves the target's construction capability rather than adding one.

What is the tradeoff between `bind` and an arrow wrapper for a frontend callback?

Both can create a callback with the behavior you need, but they express it differently. bind fixes a receiver and can prefill leading arguments. An arrow wrapper uses lexical this and can map arguments explicitly. Creating either callback produces a new function object, so recreating it repeatedly can cause identity problems for event removal, memoization, or caching. Use a stable bound function when fixed receiver behavior is important. Use a stable arrow wrapper when explicit argument handling is easier to read.

42. How do private class fields differ from conventionally private properties?Language SpecificMedium

Question Details

Define a class with a #value field and compare it with an _value property. Explain brand checks, syntax-level access restrictions, inheritance behavior, reflection and serialization visibility, and why private fields cannot be dynamically addressed by a string property name.

Short Interview Answer (30-60 seconds)

#value gives real language enforced privacy, while _value is only a normal property that follows a naming convention. Code outside the declaring class cannot directly use #value, and a subclass body cannot directly use the parent class private name. Private fields are also hidden from normal property reflection and JSON serialization. In contrast, _value can be read, changed, listed, serialized, or dynamically accessed with a string such as obj["_value"]. A private field cannot be dynamically addressed that way because #value is a private name, not a normal string property key.

Detailed Explanation

See the Code while reading this explanation.

The practical difference is simple. A name that starts with an underscore only asks other programmers not to touch it. Nothing in the language stops them from reading or changing it. A name that starts with # has a stronger rule. JavaScript itself controls where that value can be used. A child class does not automatically get permission to use the parent private name. Normal ways of listing object values do not reveal it. Converting the object to JSON does not include it either. This makes #value useful when a value must stay behind the class public behavior.

Useful Questions to Ask the Interviewer
  1. Should I compare inheritance behavior as well as direct access?
  2. Would you like me to show how reflection and JSON serialization treat both forms?
How do private class fields differ from conventionally private properties? diagram
How to Explain It in an Interview

I would start with one class that contains both #value and _value.

_value is an ordinary JavaScript property. The underscore has no special meaning to the language. Any code with the object can normally use obj._value or obj["_value"]. If it is an enumerable own property, it can appear in Object.keys and JSON.stringify. It also appears in Object.getOwnPropertyNames because that method returns ordinary own string properties whether they are enumerable or not.

#value is different. It is a private field declared by the class. Code in a place where that private name is not in scope cannot write obj.#value. JavaScript rejects that source because the private name is not available there.

JavaScript also performs a brand check when private access happens. In simple terms, the object must actually have the private field created by that class. A method cannot successfully read its private field from an unrelated object just because that object has a property with similar text.

Inheritance is strict about private names. An instance of a subclass can still contain the parent class private field because the parent constructor initializes it. Parent class methods can access that field on the subclass instance. However, code written in the subclass body cannot directly use the parent private name. If the subclass declares its own #value, that is a different private field.

Private fields are not normal properties with string keys. Therefore obj["#value"] does not reach the private field. It only looks for an ordinary public property whose string key is #value. Private fields are also absent from normal property reflection and JSON serialization.

I use #value when the class should enforce an internal boundary. I use _value when a project only needs a visible convention or when external tools intentionally need ordinary property access.

Example

The example creates one class with both forms. Public methods read and update the real private field because those methods are declared where #value is available. The underscore property is accessed directly and through a string key to show that it is an ordinary property. The example also shows that normal reflection and JSON serialization see _value but not #value. A static method uses #value in object to perform a private brand check. The subclass shows that parent methods can still read the parent private field on a subclass instance, while the subclass body cannot directly use the parent private name.

Code
class Store {
  #value;

  constructor(value) {
    // Create the private field and a normal underscore property with the same starting value.
    this.#value = value;
    this._value = value;
  }

  getPrivateValue() {
    // Access is valid because this method is declared in the class that owns #value.
    return this.#value;
  }

  setPrivateValue(value) {
    // Keep updates to the private state behind a public method.
    this.#value = value;
  }

  static hasPrivateValue(object) {
    // Check whether this object carries the private field created by Store.
    return #value in object;
  }
}

class ChildStore extends Store {
  readParentValue() {
    // The subclass body cannot directly use the parent private name, so it calls a parent public method.
    return this.getPrivateValue();
  }
}

const store = new Store(10);

console.log(store.getPrivateValue());
console.log(store._value);

// A normal property can be addressed dynamically with a string key.
console.log(store['_value']);

// This is an ordinary public property lookup. It does not reach the private field.
console.log(store['#value']);

// Reflection sees the underscore property but does not expose the private field.
console.log(Object.keys(store));
console.log(Object.getOwnPropertyNames(store));

// JSON serialization includes the enumerable underscore property but not the private field.
console.log(JSON.stringify(store));

// The private brand check is true for objects that carry the Store private field.
console.log(Store.hasPrivateValue(store));
console.log(Store.hasPrivateValue({ _value: 10 }));

const child = new ChildStore(20);

// The parent method can access the parent private field on this subclass instance.
console.log(child.readParentValue());
console.log(Store.hasPrivateValue(child));
Where it is used

Private fields are useful in production classes that keep internal state, cached values, counters, validation state, or implementation details that callers should not modify directly. They are especially useful when public methods should control how internal state is read or changed. An underscore property is useful when a team wants to mark a property as internal but still needs ordinary property access for debugging, reflection, serialization, libraries, or existing application conventions.

Why Interviewers Ask This

Interviewers ask this to check whether I understand that #value is enforced by JavaScript itself, while _value is only a naming convention. They also want to see whether I understand access rules, inheritance, visibility, brand checks, reflection, serialization, and when real language enforced privacy is useful in production code.

Common interview mistakes

A common mistake is saying that _value is private. It is not. The underscore only communicates intent. Another mistake is trying to access a private field with obj["#value"]. That searches for a normal string property and does not access the private field. Candidates also sometimes say that subclass instances do not contain parent private fields. They can contain them when the parent constructor initializes them. The important restriction is that the subclass body cannot directly use the parent private name. Another mistake is expecting Object.keys, Object.getOwnPropertyNames, or JSON.stringify to expose private fields. They do not. Finally, a private field with the same spelling declared in a subclass is a separate private field.

Interview tip

Start with the main contrast: #value is enforced by JavaScript, while _value is only a convention. Then explain direct access, inheritance, reflection, JSON serialization, string property access, and the private brand check. A small example containing both fields makes each difference easy to show.

Interviewer may ask next
What happens if a subclass declares its own `#value` when the parent class already has a `#value`?

It creates a separate private field. The subclass private name does not give the subclass access to the parent private field, even though both are written as #value. A subclass instance can contain both fields, but each declaration belongs to its own class. This matters because inheritance does not weaken the private boundary. If the subclass needs behavior that uses the parent field, it can call an accessible parent method that performs that work.

When might you choose `_value` instead of `#value` in production code?

I would choose _value when I want an internal naming convention but still need the value to behave like an ordinary JavaScript property. For example, existing tools, serializers, reflection code, or libraries may need string based property access. The tradeoff is that JavaScript does not protect _value, so callers can read or change it. I would choose #value when enforcing the class boundary is more important than that flexibility.

43. How does promise resolution assimilate thenables?Language SpecificHard

Question Details

Resolve a promise with an object whose then getter logs access and whose then method calls both resolve and reject. Explain getter errors, the single-settlement rule, recursive adoption of returned thenables, cycle rejection, and why Promise.resolve(x) may still invoke user-controlled code when x is not a native promise.

Short Interview Answer (30-60 seconds)

Promise resolution adopts thenables instead of simply fulfilling with them. JavaScript reads the value's then property first. That property access can run a getter immediately. If the getter throws, the Promise rejects. If then is callable, JavaScript calls it later through Promise job processing and adopts what it produces. Only the first resolve or reject call matters. Nested thenables are adopted recursively. Directly resolving a Promise with itself rejects with a TypeError.

Detailed Explanation

See the Code while reading this explanation.

The main idea is that some values can carry instructions for how a future result should finish. JavaScript does not always accept such a value as the final result right away. It first looks for a special part of that value. Looking at that part can itself run code. That code may fail, or it may provide instructions that later choose success or failure. Only the first choice counts. Those instructions may point to another similar value, so JavaScript can keep following them until it reaches an ordinary final value or a failure.

Useful Questions to Ask the Interviewer
  1. Should I explain both when the then getter runs and when the returned then function runs?
  2. Should I cover direct self resolution and nested thenable cycles separately?
How does promise resolution assimilate thenables? diagram
How to Explain It in an Interview

The practical rule is that Promise resolution adopts a thenable instead of automatically fulfilling with that object.

When a Promise is resolved with an object x, JavaScript reads x.then. This property access happens as part of resolving the value. A getter for then can therefore execute immediately. With Promise.resolve(x), that getter can run before Promise.resolve returns. If reading then throws, the Promise is rejected with that error.

If the retrieved then value is not callable, the Promise is fulfilled with x. If it is callable, JavaScript schedules a Promise job that later calls that function with resolve and reject functions.

Those functions follow a single settlement rule. If the thenable calls resolve first and reject second, the rejection is ignored. The first call wins even when the value passed to resolve still needs more adoption.

If resolve receives another thenable, JavaScript repeats the adoption process. This can continue through several thenables until an ordinary value is reached or one step rejects.

JavaScript also rejects direct self resolution. If a Promise is resolved with that same Promise, it rejects with a TypeError. This direct check does not mean every indirect thenable cycle is detected. A cycle formed only through separate thenables can repeatedly continue the adoption process instead.

This behavior matters at library and application boundaries. An unfamiliar object may run code through its then getter and later through its then function. Each additional thenable layer also creates more Promise job work and bookkeeping, so unnecessary deep adoption chains should be avoided.

Example

The example shows the important resolution cases with the same rules described above. The outer thenable has a then getter, so its log happens when Promise.resolve reads that property. The getter returns a then function that is invoked later. That function resolves with a nested thenable and then tries to reject. The rejection is ignored because resolve was called first. JavaScript recursively adopts the nested thenable and eventually fulfills with final value. A second object demonstrates that a throwing then getter causes rejection. A final example demonstrates direct self resolution, which rejects with a TypeError.

Code
const outerThenable = {
  get then() {
    // Reading then can immediately execute user supplied getter code.
    console.log('outer getter');

    return function (resolve, reject) {
      // JavaScript invokes this callable then later through Promise job processing.
      console.log('outer then function');

      const nestedThenable = {
        then(nestedResolve) {
          // Resolving with another thenable starts another adoption step.
          console.log('nested then function');
          nestedResolve('final value');
        },
      };

      // The first call wins, although this value still needs thenable adoption.
      resolve(nestedThenable);

      // This later rejection has no effect because resolve was already called.
      reject(new Error('ignored rejection'));
    };
  },
};

console.log('before Promise.resolve');
const adopted = Promise.resolve(outerThenable);
console.log('after Promise.resolve');

// This handler runs after the nested thenable has produced the final value.
adopted.then((value) => console.log('fulfilled:', value));

const throwingThenable = {
  get then() {
    // A failure while reading then becomes the Promise rejection reason.
    throw new Error('getter error');
  },
};

// The catch handler observes the error thrown by the then getter.
Promise.resolve(throwingThenable).catch((error) => {
  console.log('getter rejection:', error.message);
});

let resolveCycle;
const cycle = new Promise((resolve) => {
  // Save this resolver so the Promise can later be resolved with itself.
  resolveCycle = resolve;
});

// Direct self resolution rejects this Promise with a TypeError.
cycle.catch((error) => console.log('cycle:', error.name));
resolveCycle(cycle);
Where it is used

This behavior is used when application code normalizes an unknown value with Promise.resolve, when a library returns a Promise compatible object, and when one asynchronous abstraction adopts the result of another. It matters most at boundaries where values come from unfamiliar code. An object that appears to be ordinary data can execute code when its then property is read. In production, developers should also avoid unnecessary chains of custom thenables because every additional adoption step adds Promise job processing and runtime bookkeeping.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands the exact steps JavaScript follows when Promise resolution receives another object. It tests knowledge of property access, getters, Promise settlement, recursive adoption, error handling, execution timing, and direct cycle detection. It also tests production judgment because an object passed to Promise.resolve can contain user supplied behavior that runs during resolution.

Common interview mistakes

A common mistake is saying that calling resolve always fulfills a Promise immediately. Resolve can instead begin adoption of another thenable. Another mistake is assuming that both resolve and reject can change the result when a thenable calls both. Only the first call has an effect. Candidates also sometimes say that the then getter and the callable then function run at the same time. The getter can run during the synchronous resolution call, while the callable then function runs later through Promise job processing. Another mistake is saying that JavaScript detects every possible thenable cycle. The required direct check rejects a Promise resolved with itself, but separate thenables can form indirect cycles that are not caught by that same direct identity check.

Interview tip

Start with the rule that Promise resolution adopts thenables. Then explain the order clearly: read then, reject if that access throws, fulfill directly if then is not callable, call a callable then later, accept only its first resolve or reject call, recursively adopt nested thenables, and reject direct self resolution with TypeError. Mention that Promise.resolve can execute a user supplied getter before returning when given an arbitrary thenable.

Interviewer may ask next
What happens if the then getter throws before returning a function?

The Promise is rejected with the thrown error. JavaScript must read the object's then property before deciding whether the object is a thenable. If that property access throws, resolution stops and the thrown value becomes the rejection reason. The then function is never called. This matters because reading a property can execute a getter, so even inspecting an unfamiliar object's then property can run user supplied code.

Does Promise.resolve always return its argument without running user supplied code when the value looks like a Promise?

No. For an arbitrary object, Promise.resolve can read its then property, so a user supplied getter can run during that property access. If the retrieved value is callable, JavaScript later invokes it during Promise job processing. A native Promise whose constructor matches the Promise constructor being resolved has a special case and can be returned directly. This matters when normalizing values from unfamiliar libraries because generic thenables can execute code during assimilation.

44. Where can browser rendering occur relative to tasks and microtasks?Language SpecificHard

Question Details

Analyze a page that changes the DOM, queues several promise callbacks, schedules requestAnimationFrame, and sets a zero-delay timer. Explain the event-loop checkpoints at which microtasks drain, when a rendering opportunity may occur, why rendering is not guaranteed after every task, and how the frame callback relates to painting.

Short Interview Answer (30-60 seconds)

Rendering can occur after a task finishes and after the browser drains the microtask queue, when the browser reaches a rendering opportunity. It is not guaranteed to render after every task. A requestAnimationFrame callback runs during the rendering update before painting, so DOM changes made there can affect that frame. A zero delay timer creates a later task, while promise callbacks run as microtasks before the browser can move on from the current task checkpoint.

Detailed Explanation

A browser does not redraw the page immediately every time something on the page changes. It first finishes the current piece of work. It also finishes smaller pieces of waiting work that must run before it moves on. Only then does the browser get a chance to update what the user sees. It may use that chance, or it may wait until a later moment. Because of this, changing the page does not mean the new result appears on the screen immediately, and two scheduled actions do not create a guaranteed screen update between them.

Useful Questions to Ask the Interviewer
  1. Should I assume a normal visible page running on the browser main thread?
  2. Should I explain the ordering conceptually without assuming a guaranteed paint between the timer and frame callback?
Where can browser rendering occur relative to tasks and microtasks? diagram
How to Explain It in an Interview

The practical rule is: finish the current task, perform the microtask checkpoint, and then the browser may reach a rendering opportunity.

A task can be an event callback or a timer callback. JavaScript runs that task until its synchronous work finishes. The browser then performs a microtask checkpoint. Promise reaction callbacks are microtasks. If those callbacks queue more microtasks, the browser continues processing them until the microtask queue is empty.

After that checkpoint, the browser may have a rendering opportunity. This is a chance to update what is shown on the screen. It is not a promise that rendering happens after every task. The browser considers its rendering schedule and page state. For example, a frame may not yet be due, or the page may not currently need a visible update.

requestAnimationFrame is connected to the rendering process. When the browser performs the relevant rendering update, the frame callback runs before painting. DOM changes made in that callback can therefore affect the upcoming frame. The callback itself is not painting. The browser can still perform style calculation, layout, paint, and compositing work afterward as needed.

A zero delay timer works differently. It schedules a future task. It does not run immediately, and it does not create a guaranteed rendering boundary. Promise microtasks queued by the current task are processed before the browser moves beyond that task checkpoint. Depending on whether a rendering opportunity is due, the browser may perform a rendering update before a later timer task, or the timer task may run before the next rendering update.

This matters in production because long tasks and large microtask chains can keep the browser busy and delay visible frames.

Where it is used

This behavior matters in animations, loading indicators, progress updates, DOM measurement, visual transitions, and interfaces that combine promises, timers, and requestAnimationFrame. It is useful when debugging why a DOM change exists but has not appeared on screen yet. It also matters when investigating missed frames or input delays caused by long tasks or large microtask chains that postpone the browser reaching its next rendering opportunity.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands browser scheduling beyond simple queue ordering. A strong answer separates tasks, microtasks, rendering opportunities, frame callbacks, and painting. It also shows practical judgment about why long work or a large chain of promise callbacks can delay visible updates even after page content has changed.

Common interview mistakes

A common mistake is saying that the browser paints after every task. That is not guaranteed. Another mistake is treating promise reactions as normal tasks. They are microtasks and are processed at microtask checkpoints. Candidates also sometimes say requestAnimationFrame performs the paint. It does not. Its callback runs during a rendering update before painting. Another mistake is assuming a zero delay timer has a fixed order relative to the next paint. It only schedules a future task, so whether rendering occurs before that task depends on when the browser has a rendering opportunity. Finally, repeatedly creating more microtasks can delay rendering because the checkpoint must keep processing queued microtasks.

Interview tip

Give the ordering first: a task finishes, microtasks drain, and then rendering may occur. Emphasize the word may. Then explain that requestAnimationFrame runs before paint during a rendering update, while a zero delay timer is only a later task and does not create a guaranteed paint boundary.

Interviewer may ask next
What happens if each promise callback keeps queuing another promise callback?

Rendering can be delayed because the microtask checkpoint keeps processing newly queued microtasks until the queue becomes empty. If every promise callback adds another one, the browser may spend a long time processing microtasks before it can move on to a rendering opportunity. This matters because DOM changes can already exist while the user still sees an older frame. The production tradeoff is responsiveness, so code should avoid unbounded or excessively large microtask chains.

Why use requestAnimationFrame instead of a zero delay timer for visual updates?

requestAnimationFrame is usually better for work that should align with a browser rendering update. Its callback runs before painting during the relevant rendering update, so visual changes can target the upcoming frame. A zero delay timer only creates a future task and is not synchronized with the display refresh. The tradeoff is that requestAnimationFrame is intended for visual work and its execution can be reduced when the page is not visible, while timers are more appropriate for general delayed work that does not need frame timing.

45. How can an unbounded microtask chain starve the browser?Language SpecificHard

Question Details

Consider a function that recursively schedules itself with queueMicrotask while a timer and animation frame are pending. Explain why the current microtask checkpoint may never finish, which user-visible work is delayed, and how yielding through an appropriate task or scheduler boundary restores responsiveness without assuming a fixed timer delay.

Short Interview Answer (30-60 seconds)

An unbounded microtask chain can keep the browser inside the same microtask checkpoint because every microtask adds another microtask before the queue becomes empty. The browser may therefore keep delaying timers, animation frames, rendering, and user input. I would bound the amount of microtask work and periodically yield through a task boundary. That lets the event loop move on to other browser work. I would not assume that the yield or a pending timer happens after an exact amount of time.

Detailed Explanation

See the Code while reading this explanation.

The main problem is that a small piece of work can keep adding another piece of urgent work forever. The browser normally gets chances to update what the user sees and react to clicks, typing, and other actions. But if each piece creates another piece before the browser gets that chance, the browser can stay busy with the chain. A waiting timer or screen update may remain delayed even though its requested time has passed. The practical fix is to stop after a limited amount of work and give control back to the browser regularly.

Useful Questions to Ask the Interviewer
  1. Should I assume this code is running on the browser main thread?
  2. Do you want the solution to preserve frequent microtask processing while still allowing rendering and input?
How can an unbounded microtask chain starve the browser? diagram
How to Explain It in an Interview

A browser performs a microtask checkpoint after certain JavaScript work finishes. During that checkpoint, it keeps taking microtasks from the microtask queue until the queue is empty.

That rule is important here. Suppose a callback runs through queueMicrotask and schedules itself again before returning. The new microtask is added to the same microtask queue. When the browser finishes the current callback, another one is already waiting. If this continues without a limit, the queue never becomes empty, so the current checkpoint may never finish.

Other browser work can then be delayed. A pending timer callback runs as a later task, so it cannot run while the browser remains inside the endless microtask checkpoint. A pending animation frame can also be delayed because the browser does not reach a rendering opportunity. Paint, visual updates, and handling of user input can therefore appear frozen.

The production solution is to bound the work. Process only a limited number of items, then yield through a task boundary. The example uses MessageChannel to schedule that boundary. When the message task runs, the previous microtask checkpoint has ended, so the event loop can make progress between batches and the browser can take rendering opportunities when appropriate.

The exact timing is not guaranteed. Yielding gives the browser an opportunity to continue other work. It does not promise that a timer, animation frame, paint, or input callback will happen after a fixed delay.

Example

The example processes work in small batches. Each item in a batch is scheduled as a microtask, so the example still shows microtask behavior. After a fixed number of items, it awaits a Promise resolved by MessageChannel. A channel message is handled as a task, so this creates a task boundary instead of extending the current microtask checkpoint forever. The batch size controls how much work can happen before yielding. The code does not depend on a timer duration and does not claim that rendering or another callback will run at an exact moment.

Code
const pendingTaskResolvers = [];

const channel = new MessageChannel();

channel.port1.onmessage = () => {
  // Resolve one waiting yield when the browser runs this message task.
  const resolve = pendingTaskResolvers.shift();
  if (resolve) resolve();
};

function yieldToNextTask() {
  return new Promise((resolve) => {
    // Save the resolver before posting the message so this yield can finish later.
    pendingTaskResolvers.push(resolve);
    channel.port2.postMessage(null);
  });
}

function runOneMicrotask() {
  return new Promise((resolve) => {
    // Schedule one small unit through the browser microtask queue.
    queueMicrotask(resolve);
  });
}

async function processWork(totalItems, batchSize) {
  let completed = 0;

  while (completed < totalItems) {
    const batchEnd = Math.min(completed + batchSize, totalItems);

    while (completed < batchEnd) {
      // Keep each individual unit small and finish only a bounded batch.
      await runOneMicrotask();
      completed += 1;
    }

    if (completed < totalItems) {
      // Cross a task boundary so the current microtask checkpoint can end.
      await yieldToNextTask();
    }
  }

  return completed;
}

processWork(10000, 100).then((completed) => {
  // Report completion after all bounded batches have finished.
  console.log(`Completed ${completed} items`);
});
Where it is used

This matters in production code that processes large client side queues, performs repeated Promise based work, batches state updates, consumes streams, coordinates cache work, or schedules many small callbacks. Microtasks are useful for short follow up work that should happen before later tasks. They are a poor choice for an unlimited processing loop on the main thread. Long running work should be divided into bounded chunks. CPU heavy work may instead belong in a Web Worker when moving that work away from the main thread is appropriate.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands that finishing one JavaScript callback does not always let the browser immediately render or run other queued work. It tests knowledge of microtask checkpoints, task boundaries, animation frames, responsiveness, and the practical need to divide continuous work so the browser gets chances to handle rendering and input.

Common interview mistakes

A common mistake is assuming that queueMicrotask automatically gives the browser time to render. It does not. Another mistake is believing that returning from one microtask ends the checkpoint even when that microtask has already queued another one. Developers also sometimes replace the chain with Promises but keep the same unbounded behavior, because Promise reactions also use microtasks. Another mistake is assuming that a timer with zero delay runs immediately or after an exact delay. Finally, yielding after every tiny operation can create unnecessary scheduling overhead, so production code should choose a reasonable amount of work between yields.

Interview tip

Start with the key rule: the browser drains microtasks until the microtask queue is empty. Then explain that recursively adding another microtask prevents that empty state. Name the visible effects, such as delayed timers, animation frames, rendering, and input. Finish by explaining bounded work plus a task boundary, and clearly say that yielding provides an opportunity for other browser work rather than an exact timing guarantee.

Interviewer may ask next
What happens if each microtask queues two more microtasks instead of one?

The starvation problem becomes worse because the microtask queue can grow instead of merely staying nonempty. During the same checkpoint, each callback adds more work than it removes. The browser can remain unable to reach later tasks or rendering opportunities, while memory use can also increase because pending callbacks accumulate. The important behavior is still the same: the checkpoint continues while microtasks remain queued, so the code must bound the work and eventually cross a task boundary.

Why not yield after every microtask?

Yielding after every microtask would create more opportunities for other browser work, but it can add unnecessary task scheduling overhead and reduce throughput. The practical tradeoff is responsiveness versus batching efficiency. A production implementation normally processes a bounded batch of small operations and then yields. Smaller batches usually improve responsiveness, while larger batches reduce scheduling overhead. The correct batch size depends on the amount of work in each operation and the responsiveness requirements of the page.

46. What happens to the JavaScript call stack across an `await` boundary?Language SpecificHard

Question Details

Use nested async functions where the inner function awaits an already-fulfilled promise and then throws. Explain synchronous execution before the first suspension, continuation as a microtask, the returned promise chain, async stack traces as a debugging feature rather than a literal retained stack, and the caller's available handling points.

Short Interview Answer (30-60 seconds)

The current call stack does not stay in place across an await. An async function runs normally until it reaches await. It then suspends and returns control to its caller, so the active stack can unwind. Even when the Promise is already fulfilled, the code after await continues later as a microtask. If that continuation throws, the async function's returned Promise rejects. A caller can handle that rejection with await inside try and catch, or by attaching .catch() to the returned Promise.

Detailed Explanation

See the Code while reading this explanation.

Before await, JavaScript runs the nested function calls immediately, like ordinary function calls. When the inner function reaches await, it pauses at that point and gives control back. The outer function also pauses if it is waiting for the inner function. The current work can then finish and its call stack can disappear. Later, JavaScript continues the inner function. If that continued work throws an error, the failure travels through the results returned by the waiting functions. The original caller can then handle that failure instead of leaving it unhandled.

Useful Questions to Ask the Interviewer
  1. Should I assume the Promise being awaited is already fulfilled?
  2. Should I explain both try and catch with await and Promise .catch() handling?
  3. Should I also explain what browser developer tools mean when they show an async stack trace?
What happens to the JavaScript call stack across an `await` boundary? diagram
How to Explain It in an Interview

An async function executes synchronously until it reaches an await. In this example, outer() calls inner(), so both functions initially execute as normal JavaScript calls on the active call stack. inner() then executes await Promise.resolve("ready").

The Promise is already fulfilled, but await still creates a suspension boundary. JavaScript does not execute the next statement in inner() on that same active stack. inner() suspends and its async call has already returned a Promise to outer(). Because outer() awaits that Promise, outer() suspends too. Its async call has already returned its own Promise to the original caller. The active call stack can then unwind completely.

The continuation after the inner await is scheduled by Promise reaction processing as a microtask. After the current synchronous work finishes, that continuation runs during microtask processing with a new active call stack. JavaScript did not keep the old runtime stack alive and resume it later.

When inner() throws after the await, the Promise returned by inner() becomes rejected. outer() is waiting for that Promise. Because outer() does not catch the rejection, the Promise returned by outer() also becomes rejected. The original caller can handle that rejection by awaiting it inside try and catch, or by attaching .catch().

Browser developer tools may show an async stack trace that connects the related async calls. That is debugging information that preserves useful causal history. It does not mean the original runtime call stack remained active across the await.

This matters in production because error handling must follow the Promise chain. A normal synchronous try and catch around a call to outer() without awaiting its returned Promise cannot catch a rejection that happens later.

Example

The example uses outer() and inner(). outer() calls and awaits inner(). inner() logs a message, awaits an already fulfilled Promise, and then throws. The first logs happen synchronously on the original active call stack. Reaching await suspends inner(). outer() also suspends because it awaits the Promise returned by inner(). The original caller already has the Promise returned by outer(), so the remaining synchronous log statements run before the inner continuation. The inner continuation later runs as a microtask and throws. That rejects the Promise from inner(). Because outer() does not catch the rejection, its returned Promise also rejects. The caller's .catch() handles that final rejection.

Code
console.log('script start');

async function inner() {
  // This part runs immediately on the current call stack.
  console.log('inner before await');

  // The Promise is already fulfilled, but await still suspends this function.
  await Promise.resolve('ready');

  // This continuation runs later during Promise microtask processing.
  console.log('inner after await');

  // Throwing here rejects the Promise returned by inner.
  throw new Error('boom');
}

async function outer() {
  // outer also begins synchronously on the current call stack.
  console.log('outer before inner');

  // outer suspends while waiting for the Promise returned by inner.
  await inner();

  // This does not run because inner rejects before outer continues normally.
  console.log('outer after inner');
}

// Calling outer starts its synchronous portion and gives the caller its Promise.
const result = outer();

// Execution reaches here after inner and outer have suspended.
console.log('after outer call');

// The caller handles the rejection that propagates through the Promise chain.
result.catch((error) => {
  console.log('caller caught:', error.message);
});

// This synchronous work finishes before the continuation after await runs.
console.log('script end');
Where it is used

This behavior appears in frontend code whenever async functions wait for Promise based work. Common examples include waiting for fetch, reading a response body, waiting for browser storage operations, running dependent requests, and handling asynchronous user workflows. Understanding the stack boundary helps when debugging errors that happen after an await, when tracing which async function caused a rejection, and when deciding where try and catch or .catch() should be placed. It also prevents the mistake of assuming that async and await keep a normal call stack active or move JavaScript work to another thread.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands that await does not keep the current JavaScript call stack alive. They want to see whether the candidate can separate synchronous execution from later Promise continuation work, explain rejection propagation through nested async functions, and understand why developer tools can show useful async stack information even though the original runtime stack has already unwound.

Common interview mistakes

A common mistake is saying that await blocks the JavaScript thread. It does not. The async function suspends and other JavaScript work can continue. Another mistake is saying that an already fulfilled Promise makes await continue synchronously. The continuation still runs later through Promise microtask processing. Candidates also sometimes say that the original call stack is frozen until the async function resumes. The active stack actually unwinds, and the continuation later runs with a new active stack. Another mistake is treating an async stack trace as proof that one physical runtime stack survived the suspension. Developer tools can preserve useful causal async information for debugging. Finally, a synchronous try and catch around outer() without awaiting its Promise does not catch a rejection that happens later.

Interview tip

Explain the sequence in time order. First say that nested async functions run synchronously until await. Then say that await suspends the function and lets the current stack unwind. Next explain that the continuation runs as a microtask. Finish by showing how a throw becomes a Promise rejection that the caller handles with awaited try and catch or .catch().

Interviewer may ask next
Does `await` continue synchronously if the Promise is already fulfilled?

No. An already fulfilled Promise does not remove the await suspension boundary. The async function still suspends at await, and its continuation runs later through Promise microtask processing after the current synchronous stack finishes. This matters because code after the await is not part of the same active call stack. It also affects log order, error propagation, and code that depends on whether execution happens before or after the current stack completes.

Where should a caller handle an error that is thrown after an `await`?

The caller should handle the rejected Promise produced by the async operation. If the caller uses await, it can place that await inside try and catch. If the caller works directly with the Promise, it can attach .catch(). A throw after await rejects the Promise returned by that async function, and an awaiting async caller also rejects unless it catches the error. The main tradeoff is choosing the layer that has enough context to recover, report a useful failure, or convert the error into a safe result without hiding a failure that another layer needs to know about.

47. How can a circular ES module graph trigger a temporal-dead-zone error?Language SpecificHard

Question Details

Create three browser modules where a.js imports b.js, b.js imports c.js, and c.js reads a const exported by a.js during module evaluation. Trace linking and evaluation enough to identify the uninitialized live binding, then describe restructuring options that avoid eager reads without merging module roles.

Short Interview Answer (30-60 seconds)

A circular ES module graph can throw a ReferenceError when one module reads an imported const before the module that declares it has evaluated that declaration. The import is already connected to the real exported binding because ES module imports are live bindings, but that binding is still uninitialized. I would avoid reading it during module evaluation and instead read it from a function that runs after initialization, or move truly shared data into a separate dependency.

Detailed Explanation

See the Code while reading this explanation.

Three files can depend on each other in a circle. File a loads file b, file b loads file c, and file c tries to read a value from file a immediately. The browser first connects the files and the names they share. It then runs their code in dependency order. The key problem is timing. File c can reach the shared value before file a has run the line that gives the value its value. JavaScript does not return undefined in this case. The value is not ready to be read, so JavaScript throws an error.

Useful Questions to Ask the Interviewer
  1. Should c.js read the exported value immediately while the modules are being evaluated?
  2. Should the three module responsibilities remain separate when the cycle is restructured?
How can a circular ES module graph trigger a temporal-dead-zone error? diagram
How to Explain It in an Interview

ES module imports are live bindings. This means an imported name stays connected to the exported binding in the module that owns it. JavaScript does not copy the exported value when modules are linked.

Imagine that a.js imports b.js, b.js imports c.js, and c.js imports valueA from a.js. During linking, JavaScript creates the module bindings and connects the imports to the matching exports. This makes the binding known, but it does not mean the const declaration has already run.

Evaluation then follows the dependency graph. Starting from a.js, JavaScript must evaluate b.js. b.js depends on c.js, so c.js must also be evaluated. If c.js immediately reads valueA at its top level, a.js has not yet executed const valueA = 42. The binding exists, but it is still uninitialized. Reading a lexical binding such as const while it is uninitialized is a temporal dead zone access, so JavaScript throws a ReferenceError.

A circular import is therefore not automatically an error. The important question is whether code reads an uninitialized binding during evaluation.

A practical fix is to remove the eager read. c.js can export a function that reads valueA only when that function is called later. Then a.js can initialize valueA before calling through b.js. Another option is to move genuinely shared data into a separate module that is outside the cycle. Both choices can preserve clear module responsibilities. In production, keeping top level module work small and avoiding initialization that depends on partially evaluated modules makes dependency graphs easier to reason about.

Example

The example keeps three separate modules and the same circular dependency shape. a.js imports runB from b.js. b.js imports readValueA from c.js. c.js imports the live valueA binding from a.js. The important change is that c.js does not read valueA while c.js is being evaluated. It only defines readValueA. After the dependencies finish evaluating, a.js initializes valueA and then calls runB. runB calls readValueA, so the live binding is read only after valueA has been initialized. The program therefore prints 42 instead of throwing a ReferenceError.

Code
// a.js
import { runB } from './b.js';

// The binding was connected during module linking, but this declaration initializes its value.
export const valueA = 42;

// Call into the cycle only after valueA has been initialized.
runB();

// b.js
import { readValueA } from './c.js';

export function runB() {
  // This call happens after a.js has initialized valueA.
  console.log(readValueA());
}

// c.js
import { valueA } from './a.js';

export function readValueA() {
  // Read the live imported binding when this function runs, not during initial evaluation of c.js.
  return valueA;
}

// Load a.js from an HTML document as a module entry point.
// Example: <script type="module" src="./a.js"></script>
// Expected console output: 42
Where it is used

This behavior matters in frontend applications that have many ES modules, especially service modules, configuration modules, registries, state modules, and feature modules that depend on each other. A circular dependency may appear harmless until one module reads an imported lexical binding during top level evaluation. Production code is easier to maintain when module initialization has few side effects and values involved in a cycle are read only after the required declarations have been initialized.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands ES module live bindings, module linking, module evaluation, and the temporal dead zone. It tests whether the candidate can trace a circular dependency far enough to find the exact read of an uninitialized const binding. It also tests practical judgment about restructuring modules so that separate responsibilities remain while eager reads are avoided.

Common interview mistakes

A common mistake is saying that every circular ES module dependency throws an error. A cycle alone is legal. The failure depends on when a binding is read. Another mistake is expecting an imported const to contain undefined before its declaration runs. A const binding is uninitialized during its temporal dead zone, so reading it throws a ReferenceError. Another mistake is treating linking and evaluation as the same step. Linking connects imports and exports, while evaluation executes module code and initializes declarations. Developers may also hide the problem by merging unrelated modules instead of removing the eager read or extracting a genuinely shared dependency.

Interview tip

Explain the problem in two stages. First say that linking connects the live bindings without necessarily initializing their values. Then trace evaluation from a.js to b.js to c.js and identify the exact read of valueA before the const declaration in a.js has executed. Finish by showing that delaying the read until a later function call keeps the module responsibilities separate and avoids the ReferenceError.

Interviewer may ask next
Would the same circular graph throw if c.js imports valueA but does not read it during module evaluation?

No. Importing the binding by itself does not cause the temporal dead zone error. The error happens only if code reads valueA while its binding is still uninitialized. If c.js only defines a function that reads valueA later, and that function runs after a.js has evaluated the const declaration, the read succeeds. This matters because circular ES module graphs can work correctly when their evaluation does not eagerly access uninitialized bindings.

What is the main tradeoff between delaying the read and moving shared data into a separate module?

Delaying the read is usually the smaller structural change because the three existing module responsibilities can stay in place, but the circular dependency still exists and developers must understand its initialization timing. Moving genuinely shared data into a separate module can remove the cycle and make dependencies easier to understand, but it changes the module structure and adds another dependency boundary. I would extract the shared data when it naturally has an independent responsibility. Otherwise, delaying the read can be a reasonable solution.

48. What risks arise from top-level `await` in a cyclic module graph?Language SpecificHard

Question Details

Describe two modules that import each other and each wait on initialization derived from the other. Explain dependency evaluation, asynchronous module status, how a cycle can leave progress blocked or reject, and why initialization protocols should use one directional dependency or an explicit runtime handshake.

Short Interview Answer (30-60 seconds)

The main risk is blocked or failed module initialization. With top level await, importing a module can pause its evaluation until the awaited work finishes. If two modules form a cycle and each needs initialization produced by the other before its own await can finish, progress can remain pending. Depending on exactly when imported bindings are read and how the awaited promises behave, evaluation can also reject. I would avoid this design by making initialization flow in one direction or by using an explicit runtime handshake after the modules are loaded.

Detailed Explanation

The danger appears when two files depend on each other during startup, and both files also stop and wait for some work to finish. Imagine file A needs information from file B before A can finish getting ready. At the same time, file B needs information from file A before B can finish getting ready. Each side can end up waiting for progress that the other side cannot make yet. This can stop startup from completing or cause startup to fail. The safer design is to make readiness flow in one direction or coordinate it later.

Useful Questions to Ask the Interviewer
  1. Should I assume both modules use top level await during initialization?
  2. Should I focus on native ES module behavior in modern browsers?
  3. Do you want the safer initialization design as well as the failure explanation?
What risks arise from top-level `await` in a cyclic module graph? diagram
How to Explain It in an Interview

ES modules are linked before their code is evaluated. JavaScript first discovers the imports and exports and builds the dependency graph. A cycle in that graph is not automatically an error. Cyclic ES modules can work because imports are live bindings to exports from another module.

Top level await changes module evaluation because a module can become asynchronous. When evaluation reaches an await, that module may stay pending until the awaited promise settles. Modules whose evaluation depends on it may also need to wait.

Now consider module A importing module B and module B importing module A. Suppose A cannot complete its awaited initialization until B becomes ready, while B cannot complete its awaited initialization until A becomes ready. If the promises involved depend on progress that cannot occur until the other side finishes, the cycle can remain pending indefinitely. The module graph has no useful progress to make.

A related failure can happen when evaluation tries to read an imported binding before the exporting module has initialized that binding. That access can throw a ReferenceError. An awaited operation can also reject for its own reason. In either case, asynchronous module evaluation rejects, and dependent module evaluation can fail too.

This matters during frontend startup because application code may depend on those modules finishing evaluation. A pending cycle can prevent startup from completing. A rejection can send startup into its error path.

The safer design is directional initialization. One module owns initialization and another consumes its completed result. If both modules must coordinate, load them first and use explicit functions, promises, or messages as a runtime handshake. This makes readiness and failure handling visible and easier to test.

Where it is used

Top level await can be useful when a module must finish essential asynchronous setup before dependent modules use its exports. Examples include loading required configuration or completing startup data loading. The risky case is when several modules perform this setup while also depending on each other for readiness. In production, keep startup ownership clear, avoid mutual readiness dependencies, handle rejected initialization explicitly, and prefer a separate initialization function or readiness promise when coordination is complex.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands ES module evaluation, asynchronous module initialization, dependency cycles, and the production risk of making module startup depend on values that cannot become ready yet. It also tests whether the candidate can choose a safer initialization design instead of relying on a fragile cycle.

Common interview mistakes

A common mistake is saying that every cyclic ES module import causes a deadlock. That is not correct. Cyclic imports can work when evaluation does not create a waiting cycle. Another mistake is treating top level await like a separate thread. It does not move JavaScript work to another thread. It pauses asynchronous module evaluation until the awaited promise settles. Another mistake is assuming every bad cycle must stay pending. A cycle can instead reject, such as when code reads an imported binding before that binding has been initialized or when an awaited promise rejects. Candidates also sometimes assume imported values are copied snapshots, but ES module imports are live bindings. The production mistake is making two modules responsible for each other's readiness, which creates fragile startup ordering and failure behavior.

Interview tip

Start with the practical risk: asynchronous module initialization can remain pending or fail when a module cycle also becomes a readiness cycle. Then explain that ordinary cyclic imports are not automatically broken. The danger comes from mutual asynchronous initialization and from reading bindings before initialization. Finish with the design rule: keep initialization directional or coordinate explicitly after loading.

Interviewer may ask next
Does every cyclic ES module graph that uses top level await become blocked?

No. A cyclic graph does not automatically become blocked. The problem occurs when asynchronous evaluation creates a real waiting cycle where progress required by one side depends on progress that only the other side can make. Some cyclic graphs evaluate successfully. Other cases can reject instead, such as when an imported binding is read before initialization or when an awaited promise rejects. This matters because the import cycle alone is not the failure condition. The exact initialization dependency and evaluation behavior determine the result.

What is a safer production alternative when two modules need asynchronous coordination?

Use an explicit runtime handshake after the modules are loaded, or give one module ownership of initialization. For example, one module can expose an initialization function or readiness promise that another module consumes without creating mutual startup ownership. This changes the design from hidden module evaluation dependencies to visible runtime coordination. It matters because startup order, errors, retries, and readiness become easier to understand and test. The tradeoff is a little more explicit application code, but the initialization flow becomes safer and easier to maintain.

49. Why can `instanceof` fail across iframes or other realms?Language SpecificHard

Question Details

Create an array in a same-origin iframe and test it with the parent realm's Array. Explain that each realm has distinct intrinsic constructors and prototypes, how Array.isArray avoids this specific problem, and why custom classes may need explicit branding or structural checks at cross-realm boundaries.

Short Interview Answer (30-60 seconds)

instanceof can fail across realms because each realm has its own constructors and prototype objects. An array created inside an iframe can be a real array but still fail value instanceof Array when Array comes from the parent realm. For arrays, I would use Array.isArray(value). For custom classes crossing realm boundaries, I would use an explicit brand or carefully validate the required structure instead of assuming the parent constructor has the same identity.

Detailed Explanation

See the Code while reading this explanation.

A page and an iframe can each have their own JavaScript world. An array made inside the iframe belongs to that iframe world. The parent page has a different Array object. Because of this, asking whether the iframe array came from the parent Array can return false even though the value really is an array. The practical fix is to use Array.isArray when you need to recognize arrays. For your own object types, use a clear brand or validate the data you expect when values can cross between separate JavaScript worlds.

Useful Questions to Ask the Interviewer
  1. Should I assume the iframe is same origin so the parent can directly read values from it?
  2. Should I also explain how custom class instances should be checked across realm boundaries?
Why can `instanceof` fail across iframes or other realms? diagram
How to Explain It in an Interview

instanceof uses prototype identity. In normal use, value instanceof Constructor checks whether Constructor.prototype appears in the prototype chain of value.

Each JavaScript realm has its own intrinsic constructors and prototype objects. An iframe creates another realm. Its Array, Object, and other built in constructors are separate objects from the matching constructors in the parent realm.

Suppose a same origin iframe creates const items = []. That array has the iframe realm Array.prototype in its prototype chain. If the parent checks items instanceof Array, the parent realm Array.prototype is not in that chain, so the result is false.

For arrays, Array.isArray(items) is the correct cross realm check. It recognizes whether the value has the internal array nature instead of depending on the parent realm Array prototype identity.

Custom classes have the same identity problem. A class defined in the iframe and a class with the same name defined in the parent are still different constructor objects. An instance created by the iframe class normally fails instanceof against the parent class.

At a cross realm boundary, custom objects can use an explicit brand together with validation of the required data. Structural validation can also check the properties and value types that the application needs. The tradeoff is that a structural check proves that the value has an expected shape. It does not prove that one exact constructor created the value.

For a cross origin iframe, the parent cannot directly read arbitrary objects from the iframe because of the same origin policy. Communication normally uses messaging. That is a separate browser security boundary from the realm identity issue demonstrated by a directly accessible same origin iframe.

Example

The example creates a same origin iframe with srcdoc. The iframe creates an array in its own realm and stores it on the iframe window. After the iframe loads, the parent reads that array. The parent realm instanceof Array check returns false because the parent Array prototype is different from the iframe Array prototype. Array.isArray returns true because it recognizes the value as an array without depending on that prototype identity. The example also creates a custom class in each realm and shows that matching class names do not make the constructor objects identical. It then validates an explicit brand and the required data shape.

Code
const iframe = document.createElement('iframe');

// Use srcdoc so this example creates a separate iframe realm that the parent can access.
iframe.srcdoc = `
<script>
  // Create the array with this iframe realm's Array constructor and prototype.
  window.items = [1, 2, 3];

  // Define a custom class in the iframe realm and create one instance from it.
  class UserRecord {
    constructor(name) {
      this.name = name;
      this.kind = "UserRecord";
    }
  }

  window.userRecord = new UserRecord("Mina");
<\/script>`;

document.body.appendChild(iframe);

iframe.addEventListener('load', () => {
  // Read both values only after the same origin iframe has finished loading.
  const frameArray = iframe.contentWindow.items;
  const frameUser = iframe.contentWindow.userRecord;

  // The parent Array prototype is not in the iframe array's prototype chain.
  console.log(frameArray instanceof Array); // false

  // Array.isArray recognizes arrays across realm boundaries.
  console.log(Array.isArray(frameArray)); // true

  // Define a different constructor object in the parent realm.
  class UserRecord {
    constructor(name) {
      this.name = name;
      this.kind = 'UserRecord';
    }
  }

  // The iframe object was not created by this parent realm constructor.
  console.log(frameUser instanceof UserRecord); // false

  // Check the explicit brand and every piece of data this example depends on.
  const isUserRecord =
    frameUser !== null &&
    typeof frameUser === 'object' &&
    frameUser.kind === 'UserRecord' &&
    typeof frameUser.name === 'string';

  console.log(isUserRecord); // true
});
Where it is used

This behavior matters when a frontend application directly exchanges JavaScript values with same origin iframes, popup windows, or browser test environments that create separate realms. Array detection is a common case, so production code should prefer Array.isArray when the goal is to determine whether a value is an array. For application defined objects that can cross a realm boundary, explicit branding and validation can provide a stable contract without depending on one constructor object being shared.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands that separate JavaScript realms have separate built in constructors and prototype objects. It also tests whether the candidate knows why constructor identity matters to instanceof, when Array.isArray is the correct check for arrays, and how to validate custom objects that cross realm boundaries.

Common interview mistakes

A common mistake is assuming that every real array must pass value instanceof Array. That is false when the value and Array constructor come from different realms. Another mistake is assuming that two custom classes are equivalent because they have the same name or source text. Their constructor and prototype objects can still have different identities. It is also a mistake to treat one loose property check as proof that untrusted data is valid. If structural validation is used, the code should validate every property and value type that the application actually depends on. A separate mistake is forgetting that direct access to a cross origin iframe is restricted by the browser same origin policy.

Interview tip

Start with the key rule: instanceof depends on prototype identity, and each realm has separate constructors and prototypes. Then give the iframe array example. Say that Array.isArray works for cross realm array detection. Finish by explaining that custom classes often need explicit branding or structural validation when constructor identity is not a stable boundary.

Interviewer may ask next
What happens if I test the iframe array with the iframe realm's own Array constructor?

It returns true when the value is tested against the matching Array constructor from the realm that created it. For example, frameArray instanceof iframe.contentWindow.Array is true because that iframe Array prototype is in the array's prototype chain. This matters because it shows that instanceof is behaving correctly. The failure happens when the test uses a different realm constructor. In production, Array.isArray is usually clearer because array detection does not need access to the constructor from the source realm.

Should I replace every instanceof check with structural validation in production?

No. instanceof is useful when constructor identity is meaningful and the values stay within a controlled realm. The alternative is needed when values can cross realm boundaries or constructor identity is not a stable contract. For arrays, Array.isArray is the direct choice. For custom objects, explicit branding together with structural validation can be appropriate. The main tradeoff is that structural validation checks the required shape and data rather than proving that one exact constructor created the object.

50. What is Big O notation?CodingEasy

Question Details

Define Big O notation as a way to describe how an algorithm running time or extra-space use grows as the input size grows. Explain O(1), O(log n), O(n), O(n log n), and O(n squared) with small JavaScript examples. Distinguish growth rate from exact elapsed time and connect input limits to a practical solution choice.

Short Interview Answer (30-60 seconds)

Big O notation describes how an algorithm’s running time or extra-space use grows as the input size n grows. It describes growth, not exact milliseconds. Common rates are O(1), O(log n), O(n), O(n log n), and O(n²). In JavaScript, examples include direct array access, binary search, one full loop, efficient comparison-style sorting, and nested pair comparisons. I use the expected input size to decide which growth rate is practical for the problem.

Detailed Explanation

See the Code while reading this explanation.

Big O helps us understand what happens when an input becomes larger. We can use it for running time or for extra memory. It does not tell us exactly how many milliseconds a program will take. Different computers and browsers can have different measured times. Big O focuses on how the amount of work grows as n grows. The diagram compares five common growth rates using small JavaScript examples. This helps us choose a solution that will still be practical for the expected input size.

Useful Questions to Ask the Interviewer
  1. Are we analyzing running time, extra-space use, or both?
  2. What input size should the solution handle?
  3. Do you want the worst-case growth rate, the typical growth rate, or both?
What is Big O notation? diagram
How to Explain It in an Interview
1. Explain what Big O measures

Let n mean the input size. Big O describes how running time or extra-space use grows as n grows. It focuses on the growth pattern rather than exact elapsed time. Constant factors and smaller-order terms normally do not change the Big O class.

2. Explain O(1): constant growth

The diagram uses direct array access. The function first(arr) returns arr[0]. This performs one direct lookup regardless of how many items are in the array. That is why this example represents O(1) time. For a normal dense JavaScript array, indexed element access is constant-time in the usual case. The function also uses O(1) auxiliary space.

3. Explain O(log n): logarithmic growth

The diagram uses binary search on a sorted array. The search starts with low at the first index and high at the last index. It calculates the middle position. If the middle value is the target, it returns that index. If the target is larger, low moves to mid + 1. Otherwise, high moves to mid - 1. Each step removes about half of the remaining search range. That gives O(log n) time and O(1) auxiliary space for this iterative version.

4. Explain O(n): linear growth

The diagram uses a sum function. It starts total at 0 and visits every array element once. Each value is added to total. If n roughly doubles, the amount of loop work also roughly doubles. That is O(n) time. The function stores only total and the loop index, so its auxiliary space is O(1).

5. Explain O(n log n): linearithmic growth

The diagram connects O(n log n) with efficient comparison sorting and shows arr.slice().sort((a, b) => a - b) as its JavaScript example. O(n log n) grows faster than O(n) but much slower than O(n²). Merge sort and heap sort are standard examples of algorithms with O(n log n) running time. JavaScript Array.prototype.sort() itself does not have one universal Big O guarantee in ECMAScript because the specification does not require one particular sorting algorithm. Also, arr.slice() creates a copy that uses O(n) additional space.

6. Explain O(n²): quadratic growth

The diagram uses hasDuplicate(arr) with two nested loops. For each index i, the inner loop checks the later indices starting at i + 1. It returns true as soon as two equal values are found. If no duplicate is found, many pairs are compared. In the worst case, the number of comparisons grows proportionally to n². That gives O(n²) worst-case time and O(1) auxiliary space.

7. Connect input limits to the solution choice

For a small input, an O(n²) solution may still be fast enough. For larger inputs, O(n) or O(n log n) is usually more practical when the problem allows it. O(log n) and O(1) scale even better. The goal is not to choose the smallest Big O symbol without context. The goal is to choose the lowest-growth correct solution that fits the real input size, time limit, and memory limit.

Key Insight / Why This Solution Works

The key idea is to compare how different kinds of work grow as n becomes larger. O(1) stays constant. O(log n) grows slowly because each step removes a constant fraction of the remaining problem. O(n) grows directly with the number of items. O(n log n) commonly appears in efficient comparison-based sorting. O(n²) often appears when each item can be compared with many other items. The central rule is that Big O describes the growth rate of time or space, not an exact elapsed time. This makes it useful for choosing an approach that fits the expected input limits.

Code
// O(1): one direct array lookup.
function first(arr) {
  // Return the first element. The amount of work does not grow with n.
  return arr[0];
}

console.log(first([10, 20, 30, 40])); // 10

// O(log n): binary search on a sorted array.
function binarySearch(arr, target) {
  // low and high mark the part of the sorted array that can still contain target.
  let low = 0;
  let high = arr.length - 1;

  // Continue while the search interval is valid.
  while (low <= high) {
    // Check the middle position of the remaining interval.
    const mid = Math.floor((low + high) / 2);

    // Stop immediately when the target is found.
    if (arr[mid] === target) {
      return mid;
    }

    if (arr[mid] < target) {
      // The target must be to the right, so remove the left half.
      low = mid + 1;
    } else {
      // The target must be to the left, so remove the right half.
      high = mid - 1;
    }
  }

  // The target was not found.
  return -1;
}

console.log(binarySearch([10, 20, 30, 40], 30)); // 2

// O(n): visit every item once.
function sum(arr) {
  // total stores the running sum.
  let total = 0;

  for (let i = 0; i < arr.length; i += 1) {
    // Add the current value before moving to the next item.
    total += arr[i];
  }

  return total;
}

console.log(sum([10, 20, 30, 40])); // 100

// O(n log n) category shown in the diagram.
// The slice creates a copy so the original array is not changed.
// Efficient comparison sorts commonly have O(n log n) time, but ECMAScript
// does not require Array.prototype.sort() to use one specific algorithm.
const values = [40, 10, 30, 20];
const sorted = values.slice().sort((a, b) => a - b);
console.log(sorted); // [10, 20, 30, 40]

// O(n²): compare each item with every later item.
function hasDuplicate(arr) {
  for (let i = 0; i < arr.length; i += 1) {
    // Start at i + 1 so an element is not compared with itself.
    for (let j = i + 1; j < arr.length; j += 1) {
      // Stop as soon as an equal pair is found.
      if (arr[i] === arr[j]) {
        return true;
      }
    }
  }

  // No duplicate pair was found.
  return false;
}

console.log(hasDuplicate([10, 20, 30, 20])); // true
Time & Space Complexity

This question compares several complexity classes rather than giving one complexity for one algorithm. O(1) means the work stays constant as n grows. O(log n) means the work grows slowly because the remaining problem is repeatedly reduced, as in binary search. O(n) means the work grows roughly in direct proportion to n. O(n log n) grows more than linear but much less than quadratic. O(n²) grows quickly because the work can be proportional to n times n. The diagram's direct-access, iterative binary-search, sum-loop, and nested-loop examples use O(1) auxiliary space. The arr.slice() operation in the sorting example creates an O(n) copy, and the sorting implementation may use additional memory depending on the JavaScript engine.

Where it is used

Big O is useful whenever software must handle inputs that can grow. A frontend developer may use it when searching, filtering, sorting, transforming, or comparing large arrays of data. Code that feels fast with 20 items may behave very differently with 100,000 items. Big O gives a simple way to compare how well different approaches scale before relying on exact benchmark times.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate can reason about scalability instead of only checking whether code works. They want to see whether the candidate understands common growth classes, can explain why binary search is logarithmic, can distinguish one pass from pairwise nested work, and can discuss running time separately from extra memory. They also want the candidate to understand that Big O is not an exact benchmark and that input limits matter when choosing a practical solution.

Common interview mistakes
  1. Saying Big O gives an exact runtime in milliseconds. It describes a growth rate instead.
  2. Looking only at the Big O label and ignoring the expected input size. O(n²) can still be acceptable when n is small.
  3. Saying binary search is O(log n) without mentioning that its input must be sorted.
  4. Assuming every pair of nested loops is automatically O(n²). The actual number of iterations determines the complexity.
  5. Saying JavaScript Array.prototype.sort() has a guaranteed O(n log n) complexity. ECMAScript does not require one specific sorting algorithm or one universal complexity bound.
  6. Ignoring extra memory. For example, arr.slice() creates a new array whose size grows with n.
Interview tip

Explain the growth rates in order from O(1) through O(n²), connect each one to the matching JavaScript example, and then finish by explaining how the expected input size affects which complexity is practical.

Interviewer may ask next
Why can an O(n²) solution still be acceptable for a small input?

Big O describes how work grows as n increases. It does not mean every O(n²) program is immediately slow. If n is only a few dozen or a few hundred, the total number of operations may still be small enough. A quadratic solution can also be simpler. As n becomes much larger, its growth becomes expensive, so O(n) or O(n log n) is usually more practical when the problem allows it.

Does JavaScript Array.prototype.sort() always run in O(n log n) time?

No. ECMAScript defines the required sorting behavior, but it does not require every JavaScript engine to use one specific sorting algorithm or guarantee one universal Big O bound. The diagram uses sorting to represent the O(n log n) growth class because efficient comparison algorithms such as merge sort and heap sort have that complexity. If a strict bound matters, analyze the specific sorting algorithm or implementation instead of assuming it from Array.prototype.sort().

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.