Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up

JavaScript interviews concentrate on the parts of the language that behave unexpectedly. Expect closures, hoisting and the temporal dead zone, how the event loop orders microtasks and macrotasks, promises and async/await, prototypal inheritance, and how this is determined by the call site. Interviewers also test practical judgement around shallow versus deep copies, debouncing and throttling, and where to store an authentication token. The questions below cover the language fundamentals and the browser APIs that accompany them.

jobs available in Javascript
View jobs

Behavioural Questions

1. Tell me about a difficult bug you tracked down in JavaScript. How did you approach it?

Note: The interviewer is testing your debugging method, not your luck. Pick a bug that needed reasoning rather than one you fixed by chance.

Tell it as a narrowing process:

  • How you reproduced it. The hardest part of most JavaScript bugs is making them happen on demand — a race that only appears on slow connections, a state bug that only appears after a specific navigation order.
  • How you bisected. Commenting out halves, git bisect, or a breakpoint moved progressively earlier until the state was still correct.
  • The tools. Conditional breakpoints, the Network tab for a response that was not the shape you assumed, the Performance panel for a rendering problem, or simply logging the value and its typeof.
  • The root cause. Good candidates: a stale closure capturing an old value, an await inside a loop that silently serialised requests, mutation of an object someone else held a reference to, or this losing its binding in a callback.

Finish with the test you wrote so the bug could not return.

2. How do you decide whether to use a library or write something yourself?

Frame it as a cost decision rather than a matter of taste. A dependency is not free — it carries bundle size, a security surface, and a maintenance obligation.

Reach for a library when: the problem is genuinely hard and well-specified, and getting it subtly wrong is dangerous. Date and timezone handling, cryptography, rich text editing, and virtualised lists all qualify.

Write it yourself when: you need a small fraction of what the library does, the whole thing is thirty lines, or the library's API forces your architecture in a direction you do not want.

Questions worth asking out loud in the interview:

  • How large is it after tree-shaking, and does it ship to the browser or stay on the server?
  • Is it actively maintained, and how many transitive dependencies come with it?
  • How hard would it be to remove in a year?

Note: A strong closing line is that you would rather copy thirty lines into the codebase than take a dependency for them — a point made memorably by the left-pad incident.

3. Describe a time you disagreed with a teammate about a technical approach. How did it end?

Note: Choose a disagreement you lost, or one that ended in a third option. Stories where you were simply right and everyone came round make interviewers uneasy.

Give it structure:

  • The substance. State both positions fairly — for example, whether to adopt a state management library or to lift state and use context, or whether to rewrite a component or refactor it in place.
  • How you moved it out of opinion. This is the part that matters. Building a small prototype, measuring the bundle impact, or writing down the two options with their trade-offs turns an argument into a decision.
  • How it was settled. Who decided, and on what evidence.
  • How you behaved afterwards. If the decision went against you, say that you committed to it fully rather than relitigating it in code review.

Interviewers are screening for whether you can be overruled without becoming difficult to work with.

4. How do you handle code review, both giving and receiving it?

Split the answer, because the two skills are different.

Giving:

  • Separate the blocking from the optional. Say plainly which comments must be addressed and which are preferences — a reviewer who leaves twenty equally-weighted comments is unusable.
  • Ask rather than assert when you might be missing context: "what happens if this is called twice?" is more productive than "this is wrong."
  • Review the design first and the style second. Style belongs to the linter and formatter, not to a human.

Receiving:

  • Treat every comment as a signal that something was unclear, even if you disagree with the fix. If a reviewer misread it, the next reader will too.
  • Answer in the code where you can, and in a comment where the reasoning cannot be expressed in code.

Note: If you have introduced a review convention to a team — small pull requests, a template, an agreed turnaround time — mention it. It shows you improve the process rather than just working inside it.

5. The JavaScript ecosystem changes very fast. How do you keep up without chasing every new framework?

Answer with a filter, not a reading list. Anyone can name newsletters; the interesting part is how you decide what to ignore.

What to keep up with: the language and the platform. New syntax that reaches Stage 4, changes to the event loop and module semantics, and browser APIs that remove a dependency you were carrying. These are durable — they will still matter in ten years.

What to watch but not adopt: frameworks and build tools. Learn what problem each new one claims to solve, and wait until it has survived a couple of major versions and a real production story before putting it in front of users.

How to actually learn: build something small and deliberately awkward with it. Reading a tutorial teaches you the happy path; the value is in the second day, when you hit the thing the tutorial skipped.

Note: Being able to say "I evaluated X and chose not to use it, because Y" is the strongest possible answer here.

Technical Questions

1. Explain the difference between var, let and const, including hoisting and the temporal dead zone.

They differ in scope, in re-assignability, and in what happens if you touch them before the declaration.

  • var is function-scoped. It is hoisted to the top of its function and initialised to undefined, so reading it before the declaration gives you undefined rather than an error. It can be redeclared and reassigned.
  • let is block-scoped, can be reassigned, and cannot be redeclared in the same scope.
  • const is block-scoped and cannot be reassigned. It does not make the value immutable — the properties of a const object can still be changed; only the binding is fixed.

The temporal dead zone is the region from the start of the block until the let or const declaration is evaluated. The binding is hoisted but uninitialised, so accessing it there throws a ReferenceError instead of quietly returning undefined. This is a deliberate improvement: it turns a class of silent bugs into loud ones.

Note: The classic demonstration is a for loop with a setTimeout inside. With var, all callbacks log the final value because they share one binding; with let, each iteration gets a fresh binding and they log 0, 1, 2.

2. What is a closure, and where would you actually use one?

A closure is a function together with the scope it was created in. When a function is defined inside another function, it keeps a live reference to the outer function's variables — even after the outer function has returned.

function counter() {
  let count = 0;
  return function () { return ++count; };
}
const next = counter();
next(); // 1
next(); // 2

count survives because the returned function still refers to it.

Where you genuinely use them:

  • Private state. Anything the caller should not be able to reach directly — a cache, a counter, a configuration captured once.
  • Function factories. debounce and throttle both work by closing over a timer id.
  • Partial application. Capturing the first argument and returning a function that takes the rest.
  • Every React hook. useState and useEffect rely on closures, which is exactly why stale-closure bugs are the most common hooks mistake.

Note: Be ready for the follow-up on memory. A closure keeps its captured scope alive, so holding one in a long-lived structure keeps everything it captured from being collected.

Free workshop by Jobaaj Learnings

3. Explain the event loop, the call stack, and the difference between microtasks and macrotasks.

JavaScript runs on a single thread with one call stack. Anything asynchronous is handed to the host — the browser or Node — and comes back as a queued callback.

The event loop repeats one rule: when the call stack is empty, drain the microtask queue completely, then take one task from the macrotask queue.

  • Microtasks — promise callbacks (.then, the code after an await), queueMicrotask, and MutationObserver. The entire microtask queue is emptied before the next macrotask, and microtasks queued during that drain are also run.
  • MacrotaskssetTimeout, setInterval, I/O callbacks, and UI events. Exactly one runs per turn of the loop.

So this logs 1, 4, 3, 2:

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

Note: The practical consequence is that an infinite chain of microtasks starves the macrotask queue and freezes the page, whereas setTimeout always yields.

4. How does the this keyword work in JavaScript, and how do arrow functions change it?

this is decided by how a function is called, not where it is written. There are five rules, checked in order:

  • new binding — called with new, this is the newly created object.
  • Explicit bindingcall, apply or bind set it directly.
  • Method binding — called as obj.method(), this is obj. Note that this is about the call site, so pulling the method out into a variable loses it.
  • Default binding — a plain call gets the global object, or undefined in strict mode and in modules.
  • Arrow functions break the pattern entirely. They have no this of their own and inherit it lexically from the enclosing scope at definition time. call, apply and bind cannot change it.

That is exactly why arrows are the standard fix for callbacks:

// loses this
setTimeout(function () { this.tick(); }, 100);
// keeps this
setTimeout(() => this.tick(), 100);

Note: The corollary is that an arrow function should never be used as an object method or a constructor, because there is no useful this to inherit.

5. What is prototypal inheritance, and how does the prototype chain resolve a property?

Every JavaScript object has a hidden link to another object called its prototype. When you read a property, the engine looks on the object itself; if it is not there, it follows the link to the prototype, then to that object's prototype, and so on until it reaches null. That path is the prototype chain, and a missing property simply returns undefined at the end of it.

The pieces that confuse people:

  • Object.getPrototypeOf(obj) gives you an object's prototype. The legacy __proto__ accessor does the same thing.
  • A function's .prototype property is not that function's prototype. It is the object that will become the prototype of instances created with new.
  • class syntax is sugar over exactly this mechanism. extends sets up the chain; there is no separate class system underneath.

Why it matters practically: methods live on the prototype, so a thousand instances share one copy of each method rather than each carrying its own. It is also why for...in walks inherited enumerable properties and Object.keys does not.

6. Explain Promises, async/await, and how you handle errors and run work in parallel.

A Promise represents a value that is not ready yet. It is pending, then settles once as either fulfilled or rejected, and never changes again.

async/await is syntax over the same objects. An async function always returns a promise, and await pauses that function until the promise settles without blocking the thread.

Error handling: wrap the await in try/catch. The trap is that an async function's rejection does not propagate to the caller unless the caller awaits it too — a forgotten await produces an unhandled rejection that is easy to miss.

Running work in parallel is the point most candidates get wrong. This is sequential and takes as long as both requests combined:

const a = await getA();
const b = await getB();

This starts both immediately:

const [a, b] = await Promise.all([getA(), getB()]);

The combinators: Promise.all rejects as soon as any input rejects; Promise.allSettled always waits for all of them and reports each outcome; Promise.race settles with the first to settle, useful for timeouts; Promise.any resolves with the first to succeed.

7. What is the difference between == and ===, and what are the falsy values in JavaScript?

=== is strict equality: if the two operands are of different types the result is false, with no conversion. == is loose equality: it coerces the operands to a common type first, following a set of rules that produce genuinely surprising results.

  • 0 == '0' is true, 0 === '0' is false.
  • null == undefined is true, but null == 0 is false — null is loosely equal only to undefined and itself.
  • NaN is not equal to anything, including itself. Use Number.isNaN to test for it.

The eight falsy values — everything else is truthy:

  • false, 0, -0, 0n, '', null, undefined, NaN

Note: An empty array and an empty object are both truthy, which catches people out constantly. The one place == is genuinely useful is x == null, which tests for null or undefined in a single check — and the modern alternatives are the ?? and ?. operators.

8. What is event delegation, and how do event bubbling and capturing work?

A DOM event travels in three phases: it captures down from the window to the target, fires at the target, then bubbles back up to the window. addEventListener attaches to the bubbling phase by default; passing true or { capture: true } attaches to the capturing phase instead.

Event delegation uses bubbling deliberately. Instead of attaching a listener to every item, you attach one to a common ancestor and inspect event.target:

list.addEventListener('click', (e) => {
  const item = e.target.closest('li');
  if (!item || !list.contains(item)) return;
  handle(item.dataset.id);
});

Why it is worth doing:

  • One listener instead of hundreds — less memory and a faster initial render.
  • It works for elements added later, with no need to rebind after every update.

Note: Know the difference between stopPropagation, which halts the journey up the tree, and preventDefault, which cancels the browser's default action such as following a link. They are unrelated, and reaching for stopPropagation is usually a sign that something else is wrong.

9. What is the difference between a shallow copy and a deep copy, and how do you make each one?

Objects and arrays are held by reference, so assigning one variable to another gives you two names for the same thing. Copying is how you break that.

A shallow copy duplicates the top level only. Nested objects are still shared:

const copy = { ...original };
const copy2 = Object.assign({}, original);
const arr2 = [...arr];   // or arr.slice()

Change copy.user.name and the original changes too, because user is the same object in both.

A deep copy duplicates every level:

  • structuredClone(original) is the modern built-in answer. It handles Dates, Maps, Sets, TypedArrays, and circular references.
  • JSON.parse(JSON.stringify(original)) is the old trick, and it is lossy — it silently drops functions and undefined, turns Dates into strings, and throws on circular references.
  • A library such as lodash's cloneDeep when you need fine control.

Note: The reason this is asked so often is React and Redux state. Mutating nested state in place means the reference does not change, so the component does not re-render.

10. Explain debouncing and throttling, and when you would use each.

Both limit how often a function runs, but they answer different questions.

Debounce — wait until the activity stops, then run once. Every new call resets the timer.

function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

Throttle — run at most once per interval, no matter how many calls arrive.

function throttle(fn, limit) {
  let waiting = false;
  return (...args) => {
    if (waiting) return;
    fn(...args);
    waiting = true;
    setTimeout(() => { waiting = false; }, limit);
  };
}

Which to use:

  • Debounce when only the final state matters — a search-as-you-type box, validating a field after the user stops typing, saving a draft.
  • Throttle when you need regular updates during the activity — scroll position, mouse move, an infinite-scroll trigger, window resize.

Note: Debouncing a scroll handler is a classic mistake: nothing happens at all until the user stops scrolling.

Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up as