Blog

JavaScript Interview Questions

Key Takeaways

  • Closures retain access to lexical bindings, not frozen copies of their values.
  • For ordinary functions, the call site determines this; arrow functions inherit it from the surrounding scope.
  • In browsers, queued promise reactions and other microtasks run before the next task, including an eligible timer callback.
  • const prevents reassignment, not object mutation. Strict equality avoids type coercion, and ES module imports are live bindings.

For a JavaScript interview, prepare to explain scope, closures, this, asynchronous execution, prototypes, equality, and modules—and predict small programs using those rules. These JavaScript interview questions pair concise answers with runnable examples so you can demonstrate understanding rather than recite definitions.

For each example, predict the output first, then explain why it happens and how you'd avoid the underlying bug in production. Run each single-file sample separately in a modern browser console; the module example includes file-based Node.js instructions.

Scope and Hoisting Interview Questions: var, let, and const

Question: How do the three variable declarations differ?

Inside a function, var is function-scoped, while let and const are block-scoped. You can reassign var and let bindings; you can't reassign a const binding.

DeclarationScope inside functionsReassignmentInitial binding state
varFunctionAllowedInitialized to undefined
letBlockAllowedUninitialized until declaration executes
constBlockNot allowedUninitialized until declaration executes

Hoisting describes how bindings are established before execution reaches their declarations. A var binding initially contains undefined; accessing a let or const binding before initialization throws a ReferenceError. That inaccessible period is the temporal dead zone, or TDZ.

function inspectScope() {
  console.log(total); // undefined
  var total = 1;

  {
    let total = 2;
    console.log(total); // 2
  }
  console.log(total); // 1

  try {
    console.log(label);
    let label = "ready";
  } catch (error) {
    console.log(error.name); // ReferenceError
  }

  const state = { ready: false };
  state.ready = true;
  console.log(state.ready); // true
}

inspectScope();

The inner total shadows the outer binding: the same name refers to a different variable inside that block. The object mutation succeeds because const protects the binding, not the object's properties. Prefer const by default and let when reassignment is intentional. For more comparisons, see JavaScript var, let, and const.

Closures: What Does a Function Remember?

Question: What is a closure, and why does it matter?

A closure is a function together with access to its surrounding lexical environment—the variables available where the function was created. That access can survive after the enclosing function returns. The function observes bindings, not a snapshot taken at creation time.

function createCounter() {
  let count = 0;
  return () => ++count;
}

const next = createCounter();
const independent = createCounter();
console.log(next());        // 1
console.log(next());        // 2
console.log(independent()); // 1

Each call to createCounter() creates a separate count binding. Calls to the same returned function share that particular binding. This makes closures useful for encapsulating state and building callbacks.

Follow-up: Why does this loop produce three identical values?

function createReaders() {
  const readers = [];
  for (var i = 0; i < 3; i++) {
    readers.push(() => i);
  }
  return readers;
}

console.log(createReaders().map(read => read()).join(","));
// 3,3,3

All three functions capture the same function-scoped i, which is 3 when they're called. Replacing var with let produces 0,1,2 because each iteration receives a separate i binding. The issue is shared state, not timers or asynchronous execution.

this Binding: Why Does the Call Site Matter?

Question: How is this determined?

For an ordinary, unbound function, look at how it's called. A method call such as user.getThis() supplies user as the receiver; a standalone call supplies undefined in strict mode. Arrow functions instead inherit this from their surrounding lexical context.

"use strict";

const user = {
  name: "Ada",
  getThis() {
    return this;
  },
  makeReader() {
    return () => this.name;
  }
};

const detached = user.getThis;
console.log(user.getThis() === user); // true
console.log(detached());              // undefined
console.log(detached.call({ name: "Lin" }).name); // Lin

const bound = detached.bind(user);
console.log(bound().name); // Ada

const readName = user.makeReader();
console.log(readName.call({ name: "Lin" })); // Ada

call() and apply() supply a receiver for an invocation; bind() creates a new function with a bound receiver. They cannot replace an arrow function's inherited this. With new, an ordinary constructor starts with this bound to the new instance; returning another object can replace that instance as the result.

Event Loop Interview Questions: Microtasks vs. Macrotasks

Question: What runs first: synchronous code, promises, or timers?

The current synchronous execution finishes first. At a browser microtask checkpoint, queued microtasks run until the queue is empty, including newly added microtasks, before the next task starts. Promise reactions and queueMicrotask() callbacks use microtasks; timer callbacks run as tasks, often informally called macrotasks.

console.log("start");

setTimeout(() => console.log("timer"), 0);

new Promise(resolve => {
  console.log("executor");
  resolve();
}).then(() => console.log("promise"));

queueMicrotask(() => console.log("microtask"));
console.log("end");

// start
// executor
// end
// promise
// microtask
// timer

The promise executor runs synchronously during construction. Its then() callback runs later, and in this example it enters the microtask queue before the explicit queueMicrotask() callback. A zero-delay timer does not interrupt the current execution or bypass those microtasks.

Current synchronous execution
            |
            v
Drain microtasks, including newly queued microtasks
            |
            v
Browser may render; subsequent task can run

Ask which runtime the question assumes. Node.js has event-loop phases and a separate process.nextTick() queue, so don't treat a browser scheduling diagram as a complete Node.js model. Compare the official Node.js scheduling explanation with our Node.js event loop walkthrough.

Promises and async/await: Results and Errors

Question: What does an async function return, and what does await pause?

A promise represents an eventual outcome and can be pending, fulfilled, or rejected. An async function always returns a promise. Its body runs synchronously until suspension at an await; awaiting pauses that function's continuation, not the entire JavaScript thread. An awaited rejection throws at the await expression and can be handled with try/catch.

// Self-contained stand-in for a promise-based data source.
async function readUser(id) {
  if (id < 1) throw new Error("Invalid user ID");
  return { id, name: `User ${id}` };
}

async function main() {
  try {
    const [first, second] = await Promise.all([
      readUser(1),
      readUser(2)
    ]);
    console.log(first.name, second.name); // User 1 User 2

    await readUser(0);
  } catch (error) {
    console.log(error.message); // Invalid user ID
  }
}

main().catch(console.error);

For independent asynchronous operations, start them together and aggregate their promises rather than unnecessarily waiting before starting the next operation. Promise.all() preserves input order in its result array and rejects when an input rejects; it doesn't cancel the remaining operations. Use Promise.allSettled() when you need every outcome.

Follow-up: then() returns a new promise. Returning a value fulfills that next promise with the value; throwing rejects it; returning a promise makes the chain adopt its outcome. Remember to return or await asynchronous work so its completion and errors remain connected to the caller. See our JavaScript async/await examples for more patterns.

Advertisement

Prototypal Inheritance: Delegation, Not Copying

Question: How does JavaScript find an inherited property?

Property lookup checks the object itself, then follows its prototype chain until it finds the property or reaches null. Inheritance delegates lookup; it doesn't copy the prototype's properties onto every object. Object.create(base) creates an object whose prototype is base.

const base = {
  role: "engineer",
  describe() {
    return `${this.name}: ${this.role}`;
  }
};

const person = Object.create(base);
person.name = "Ada";
console.log(person.describe()); // Ada: engineer
console.log(Object.hasOwn(person, "role")); // false

person.role = "lead";
console.log(base.role, person.role); // engineer lead
person --> base --> Object.prototype --> null

Here, assigning person.role creates an own property that shadows the inherited writable data property. The inherited method still receives person as this when called as person.describe(). Object.hasOwn() checks only own properties, not inherited ones.

Class syntax uses this same prototype-based inheritance model. Don't confuse a constructor's prototype property with an instance's internal prototype link. Review JavaScript classes and constructors to connect the two forms.

Equality: == vs. ===

Question: Does strict equality compare objects by their contents?

No. === compares without type coercion; == applies conversion rules when operand types differ. When both operands are objects, both operators compare identity, not structural contents. Prefer strict equality unless you intentionally need a particular coercion rule.

console.log(0 == false);        // true
console.log(0 === false);       // false
console.log(null == undefined); // true
console.log({ id: 1 } === { id: 1 }); // false

const shared = { id: 1 };
console.log(shared === shared); // true

console.log(NaN === NaN);           // false
console.log(Object.is(NaN, NaN));   // true
console.log(0 === -0);              // true
console.log(Object.is(0, -0));      // false

Object.is() differs from strict equality for NaN and signed zero: it considers NaN equal to itself and distinguishes positive from negative zero. It isn't a deep-equality function either.

Modules: Imports, Exports, and Live Bindings

Question: Is an imported variable a copied value?

No. ES module imports are read-only live bindings: the importer sees changes made by the exporting module but cannot reassign the imported binding. Modules also have their own scope, automatically use strict mode, and have undefined as top-level this.

Create these two files in the same directory. Node.js recognizes the .mjs extension as an explicit ES module marker, so this example needs no package configuration.

// counter.mjs
export let count = 0;
export function increment() {
  count += 1;
}
// main.mjs
import { count, increment } from "./counter.mjs";

console.log(count); // 0
increment();
console.log(count); // 1
console.log(this);  // undefined
node main.mjs

The second read observes the updated exported binding. Trying to assign to count from main.mjs would throw a TypeError; calling the exported function lets the exporting module update its own binding.

Common Mistakes

  • Giving output without the rule. Explain which scope, receiver, or queue determines it.
  • Changing the question's environment. Ask whether the snippet runs as a browser script, an ES module, or Node.js.
  • Ignoring failure paths. Show where a rejected promise is handled, not just the success log.
  • Optimizing for trivia. After predicting behavior, explain how you'd make the code clearer in production.

FAQ

What is a closure in JavaScript?

A closure is a function together with access to its surrounding lexical environment. It can continue accessing those bindings after the enclosing function returns, and it observes their current values rather than frozen copies.

Does await block JavaScript execution?

Await suspends the current async function's continuation, not the entire JavaScript thread. Other eligible work can run while that function waits; marking CPU-heavy code async does not move it to another thread.

What runs first: a promise callback or setTimeout?

If a reaction to an already-fulfilled promise and a timer are scheduled during the same synchronous browser task, the promise reaction runs first. It runs as a microtask before the timer callback's task, after the current synchronous execution finishes.

Does const make an object immutable?

No. Const prevents reassignment of the variable binding, but an object's properties can still change unless another mechanism restricts them. Binding immutability and object immutability are different guarantees.

Further Reading

Advertisement

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me

Advertisement