Skip to main content
PProfileMatch

JavaScript Interview Questions and Answers

26 questions8 beginner · 12 intermediate · 6 advancedLast reviewed

This set is for front-end, full-stack and Node.js candidates, from first-job screens to senior rounds. It focuses on the questions that keep coming back: scoping, closures, this, the event loop, promises, and a few TypeScript questions that frequently follow.

Practise by answering aloud in about a minute, then read the model answer and the follow-up. Paste each snippet into Node or the browser console and predict the output before running it. For a structured walkthrough, the JavaScript fundamentals course and the TypeScript course cover the same ground.

  1. 1What are the differences between var, let and const?Beginner

    var is function-scoped, can be redeclared, and is hoisted and initialised to undefined. let and const are block-scoped, cannot be redeclared in the same scope, and sit in the temporal dead zone until their declaration runs. const forbids reassigning the binding, but it does not freeze the value.

    In modern code, default to const, use let when you must reassign, and avoid var. The const point trips people up: a const object or array can still be mutated, because only the variable's link to it is fixed. Use Object.freeze if you want shallow immutability.

    What the interviewer is checking: Scope rules plus the common misunderstanding that const means immutable.

    Likely follow-up: Where does a variable declared with var inside an if block end up visible?

  2. 2What is hoisting, and what is the temporal dead zone?Beginner

    Hoisting means declarations are processed before code runs, so a name exists from the start of its scope. Function declarations are hoisted with their body, so you can call them before they appear. var is hoisted and initialised to undefined, so reading it early gives undefined.

    let and const are also hoisted, but not initialised until execution reaches the declaration. Accessing them in between throws a ReferenceError; that gap is the temporal dead zone. It turns a silent undefined bug into a loud error, which is one reason to prefer them. Function expressions assigned to a variable follow the variable's rules, not the function-declaration rules.

    javascript
    console.log(early);          // undefined, not an error
    var early = 1;
    
    function hoisted() { return "ok"; }
    console.log(hoisted());
    
    try {
      console.log(tdz);
    } catch (e) {
      console.log(e.name + ": " + e.message);
    }
    let tdz = 1;

    Output

    undefined
    ok
    ReferenceError: Cannot access 'tdz' before initialization

    What the interviewer is checking: Whether you describe hoisting accurately instead of saying code moves to the top.

    Likely follow-up: Is a function expression like const f = function(){} hoisted the same way as a declaration?

  3. 3Explain the difference between == and ===. Which do you use and why?Beginner

    === compares without conversion: different types are never equal. == first coerces operands to a common type using a set of rules, which produces surprising results such as 0 == "" being true and [] == false being true.

    Default to === everywhere so behaviour is predictable. The one idiom some teams accept is x == null, which is true for both null and undefined. Remember also that NaN is not equal to anything, including itself, so use Number.isNaN or Object.is for that check. Object.is also distinguishes 0 from -0.

    What the interviewer is checking: Awareness of coercion pitfalls and a sensible team convention.

    Likely follow-up: Why is null >= 0 true while null == 0 is false?

  4. 4What is the difference between null and undefined, and how do ?? and ?. help?Beginner

    undefined means a value was never assigned, such as a missing property or an omitted argument. null is an explicit "no value" that a programmer assigns. typeof null is "object", a long-standing quirk of the language, so check for it with === null.

    ?. short-circuits to undefined when the left side is null or undefined, avoiding long guard chains. ?? falls back only for null or undefined, unlike ||, which also replaces 0, "" and false. That makes ?? the correct way to default a count of zero or an empty string that is a valid value.

    What the interviewer is checking: Whether you pick ?? over || for the right reason.

    Likely follow-up: What does a ??= 5 do compared to a ||= 5?

  5. 5What is a closure? Give a practical use.Beginner

    A closure is a function bundled with the variables from the scope where it was created, which stay accessible even after that outer function has returned. The inner function keeps a live reference to those variables, not a copy of their values.

    A practical use is private state: a factory returns functions that share a variable no outside code can touch. Closures also power callbacks, memoisation, partial application and module patterns. The risk is accidentally retaining large objects for as long as the function lives, which is a classic source of memory leaks.

    javascript
    function makeCounter() {
      let n = 0;
      return { inc: () => ++n, get: () => n };
    }
    const c = makeCounter();
    c.inc(); c.inc();
    console.log(c.get());   // 2
    console.log(c.n);       // undefined: n is not reachable from outside

    Output

    2
    undefined

    What the interviewer is checking: A precise definition plus a concrete, non-trivial use.

    Likely follow-up: How could a closure cause a memory leak?

  6. 6How does this get its value in JavaScript?Beginner

    For normal functions, this is decided by how the function is called, not where it is defined. obj.method() sets this to obj. A plain call fn() gives undefined in strict mode (or the global object in sloppy mode). new Fn() creates a new object as this. call, apply and bind set it explicitly.

    The classic bug is detaching a method and passing it as a callback, which loses this. Fix it with bind, an arrow function wrapper, or by defining the handler as an arrow-function property. Arrow functions do not have their own this; they use the one from the surrounding scope.

    javascript
    "use strict";
    const desk = {
      label: "D1",
      regular() { return this?.label; },
      delayed() { return [1].map(() => this.label); },
    };
    console.log(desk.regular(), desk.delayed());
    const detached = desk.regular;
    console.log(detached());
    console.log(desk.regular.call({ label: "D2" }));
    const bound = desk.regular.bind({ label: "D3" });
    console.log(bound.call({ label: "x" }));

    Output

    D1 [ 'D1' ]
    undefined
    D2
    D3

    What the interviewer is checking: The call-site rule, and awareness of lost-this bugs and arrow-function behaviour.

    Likely follow-up: Can you rebind this on an arrow function using call?

  7. 7Why does [10, 1, 5].sort() return [1, 10, 5], and how do you sort numbers properly?Beginner

    Without a comparator, sort converts elements to strings and compares them in UTF-16 order, so "10" sorts before "5". For numbers, pass a comparator: (a, b) => a - b for ascending order. The comparator returns a negative, zero or positive number.

    Also note that sort mutates the original array. To avoid that, copy first with [...nums].sort(...), or use toSorted, available in recent Node and browser versions. Typical array companions are map to transform, filter to select and reduce to combine; always give reduce an initial value, or it throws on an empty array.

    What the interviewer is checking: Knowing default sort semantics and mutation, rather than just using sort blindly.

    Likely follow-up: Which array methods mutate the original array and which return a new one?

  8. 8Why does 0.1 + 0.2 === 0.3 return false in JavaScript?Beginner

    JavaScript numbers are IEEE 754 double-precision floats. Many decimal fractions, such as 0.1 and 0.2, have no exact binary representation, so the sum carries a tiny rounding error and equals 0.30000000000000004 rather than exactly 0.3. This is not unique to JavaScript.

    For comparisons, check that the difference is within a small tolerance such as Number.EPSILON for values near 1. For money, avoid floats: store integer minor units like cents, or use a decimal library. Integers are exact up to Number.MAX_SAFE_INTEGER; beyond that, use BigInt.

    What the interviewer is checking: Whether you know it is a floating-point property and how to handle money safely.

    Likely follow-up: What does toFixed return, and why can it surprise you?

  9. 9What is the difference between arrow functions and regular functions?Intermediate

    Arrow functions have no own this, arguments, super or new.target; they take them from the enclosing scope. They also cannot be used as constructors with new, and they have no prototype. Regular functions have all of these.

    That makes arrows ideal for callbacks and array methods inside a method, where you want to keep the outer this. Avoid them as object methods that need this to be the object, and as prototype methods or event handlers that rely on the element being this. Use rest parameters (...args) in place of arguments in arrows.

    What the interviewer is checking: That you tie the difference to concrete consequences, not just shorter syntax.

    Likely follow-up: What does an arrow function return if its body is wrapped in braces with no return?

  10. 10How does prototypal inheritance work, and how do ES6 classes relate to it?Intermediate

    Every object has an internal link to a prototype object. When you read a property, the engine looks on the object, then on its prototype, then the prototype's prototype, until it finds it or reaches null. Methods shared by all instances live on the prototype, not on each instance.

    ES6 class is syntax over this mechanism. Methods go onto Class.prototype, extends links the prototype chains, and super calls up the chain. Differences worth knowing: class constructors throw if called without new, and class bodies run in strict mode. Object.create(proto) is the direct way to build an object with a chosen prototype.

    What the interviewer is checking: Whether you understand the chain lookup rather than treating classes as a separate system.

    Likely follow-up: What is the difference between Object.keys and a for...in loop with respect to the prototype chain?

  11. 11Why does a loop with var and setTimeout print the same number each time, and how does let fix it?Intermediate

    var is function-scoped, so the whole loop shares a single i. Every callback closes over that one variable, and by the time they run the loop has ended and i holds its final value.

    let in a for header creates a fresh binding for each iteration, so each callback captures its own value. Before let existed, people wrapped the body in an immediately invoked function to create a new scope. The deeper lesson is that closures capture variables, not values, so always ask which variable a callback is actually referring to.

    javascript
    const withVar = [];
    for (var i = 0; i < 3; i++) withVar.push(() => i);
    console.log(withVar.map(f => f()));
    
    const withLet = [];
    for (let j = 0; j < 3; j++) withLet.push(() => j);
    console.log(withLet.map(f => f()));

    Output

    [ 3, 3, 3 ]
    [ 0, 1, 2 ]

    What the interviewer is checking: Applying the closure model to a familiar puzzle and explaining why, not just the fix.

    Likely follow-up: Name another way to fix this without using let.

  12. 12Explain the event loop. In what order do synchronous code, promise callbacks and setTimeout callbacks run?Intermediate

    JavaScript runs on one thread with a call stack. Async work such as timers and I/O is handled by the host environment, and finished callbacks are queued. When the stack is empty, the loop runs all pending microtasks (promise reactions, queueMicrotask, code after await), then takes the next macrotask (a timer, an I/O callback, a UI event).

    So the order is: current synchronous code, then every queued microtask, then the next macrotask. A setTimeout(fn, 0) therefore runs after promise callbacks, and the delay is a minimum, not a guarantee. A long synchronous loop blocks everything, including rendering and timers.

    javascript
    console.log("1 sync");
    setTimeout(() => console.log("5 timeout"), 0);
    Promise.resolve().then(() => console.log("3 promise"));
    queueMicrotask(() => console.log("4 microtask"));
    (async () => {
      console.log("2 sync part of async fn");
      await null;
      console.log("4b after await");
    })();
    console.log("2b sync end");

    Output

    1 sync
    2 sync part of async fn
    2b sync end
    3 promise
    4 microtask
    4b after await
    5 timeout

    What the interviewer is checking: A correct model of microtasks versus macrotasks and the fact that async functions start synchronously.

    Likely follow-up: Can microtasks starve the event loop? Give an example.

  13. 13What is the difference between Promise.all, allSettled, race and any?Intermediate

    Promise.all resolves with all results once every promise fulfils, but rejects immediately on the first rejection. allSettled waits for everything and reports each outcome as fulfilled or rejected, so it never rejects. race settles with the first promise to settle, success or failure. any fulfils with the first fulfilment and rejects only if all reject.

    Choose by failure policy: all for all-or-nothing, allSettled for independent best-effort work like loading dashboard widgets, race for timeouts, any for the fastest successful mirror. Note that when all rejects, the other operations are not cancelled, they just have their results ignored.

    What the interviewer is checking: Picking the combinator from the failure semantics you need.

    Likely follow-up: How would you add a timeout to a fetch using one of these?

  14. 14What is wrong with return load() inside a try/catch in an async function, and with forEach(async ...)?Intermediate

    return load() returns the promise without awaiting it, so if it rejects, the rejection happens after the function has left the try block and your catch never sees it. Use return await load() when you want to catch errors locally.

    forEach ignores the promises returned by an async callback, so it neither waits for them nor surfaces their errors; code after the loop runs first. Use for...of with await for sequential work, or await Promise.all(items.map(...)) to run in parallel. Any promise that is created and not awaited or caught risks an unhandled rejection.

    javascript
    async function load() { throw new Error("db down"); }
    
    async function a() {
      try { return load(); } catch (e) { return "caught-a"; }
    }
    async function b() {
      try { return await load(); } catch (e) { return "caught-b"; }
    }
    
    a().then(console.log).catch(e => console.log("a rejected:", e.message));
    b().then(console.log);
    [1, 2, 3].forEach(async n => { await null; console.log("forEach", n); });
    console.log("after forEach");

    Output

    after forEach
    forEach 1
    forEach 2
    forEach 3
    caught-b
    a rejected: db down

    What the interviewer is checking: Real-world async/await error-handling judgement, not just syntax.

    Likely follow-up: When is sequential awaiting in a loop the right choice over Promise.all?

  15. 15How do you copy an object in JavaScript? When do spread, JSON and structuredClone differ?Intermediate

    Spread ({ ...obj }) and Object.assign make a shallow copy: nested objects are shared, so mutating a nested field affects both. For a deep copy, structuredClone handles nested objects, arrays, Date, Map, Set and cyclic references, but throws on functions.

    The JSON.parse(JSON.stringify(x)) trick is lossy: it drops functions and undefined, turns Date into a string, and fails on cycles and BigInt. For nested state updates, such as in a UI store, a common approach is to copy only the path you change rather than deep-cloning everything.

    javascript
    const o = { id: 1, when: new Date(0), nested: { k: 1 }, fn() {} };
    const sh = { ...o };
    sh.nested.k = 2;
    console.log(o.nested.k);
    
    const sc = structuredClone({ when: new Date(0), nested: { k: 1 } });
    console.log(sc.when instanceof Date);
    
    console.log(typeof JSON.parse(JSON.stringify(o)).when);
    try { structuredClone(o); } catch (e) { console.log(e.name); }

    Output

    2
    true
    string
    DataCloneError

    What the interviewer is checking: Knowing the trade-offs of each copy approach, especially the hidden losses in JSON cloning.

    Likely follow-up: How would you update one nested field immutably without cloning the whole object?

  16. 16What is the difference between debouncing and throttling? Implement a debounce.Intermediate

    Debounce delays a call until the events have stopped for a given time, so a burst produces a single call at the end. It suits search-as-you-type or autosave. Throttle guarantees a call at most once per interval while events continue, so it suits scroll or resize handlers where you still want regular updates.

    A debounce keeps a timer in a closure: each call clears the old timer and sets a new one. Mention edge cases: leading versus trailing execution, a cancel method for cleanup, and preserving this and arguments. In React, create the debounced function once, not on every render.

    javascript
    function debounce(fn, ms) {
      let id;
      return (...a) => { clearTimeout(id); id = setTimeout(() => fn(...a), ms); };
    }
    function throttle(fn, ms) {
      let last = -Infinity;
      return (...a) => {
        const now = Date.now();
        if (now - last >= ms) { last = now; fn(...a); }
      };
    }
    const d = debounce(x => console.log("debounced", x), 50);
    const t = throttle(x => console.log("throttled", x), 50);
    for (let i = 1; i <= 5; i++) { d(i); t(i); }
    setTimeout(() => t(6), 80);

    Output

    throttled 1
    debounced 5
    throttled 6

    What the interviewer is checking: Clear distinction of the two patterns plus a working closure-based implementation.

    Likely follow-up: How would you add a leading-edge option to your debounce?

  17. 17What is the difference between CommonJS and ES modules?Intermediate

    CommonJS uses require() and module.exports, loads synchronously and evaluates at runtime, so you can call require conditionally. ES modules use static import and export, which are analysed before execution. That enables tree-shaking, top-level await and live bindings, where an imported name reflects later changes in the exporting module.

    In an ES module file, require, module and __dirname do not exist, and import.meta.url replaces them. In Node, a file is treated as ESM based on the .mjs extension or "type": "module" in package.json. You can load an ES module from CommonJS using dynamic import(), which returns a promise.

    What the interviewer is checking: Understanding static versus dynamic loading and the practical interop rules.

    Likely follow-up: Why does tree-shaking work with ES modules but not reliably with CommonJS?

  18. 18What are #private class fields, and how are they different from the underscore convention?Intermediate

    A field named with #, like #raw, is truly private to the class body: outside code cannot read or write it, and the engine enforces this. A leading underscore is only a naming convention, and anyone can still access obj._raw. Private fields also do not appear in Object.keys or JSON.stringify.

    Accessing obj.#raw outside the class is a syntax error, not just undefined. Combine private fields with getters and setters to validate changes. Before this feature, closures or WeakMap were used for the same purpose. Static fields, written with static, belong to the class itself rather than each instance.

    What the interviewer is checking: Familiarity with modern class features and real encapsulation.

    Likely follow-up: What does a getter without a setter do when you assign to the property?

  19. 19In TypeScript, what is the difference between type and interface? Which do you choose?Intermediate

    Both describe object shapes and are largely interchangeable for that. Interfaces can be extended with extends and can be reopened and merged through declaration merging, which is how libraries let you augment their types. A type alias can also name unions, intersections, tuples, primitives and mapped or conditional types, which an interface cannot.

    A practical rule: use interface for public object shapes and class contracts, especially in libraries, and type when you need unions or computed types. Whatever you pick, be consistent in the codebase. Neither exists at runtime, since types are erased when the code compiles to JavaScript.

    What the interviewer is checking: Knowing the real differences instead of repeating that they are the same.

    Likely follow-up: What does it mean that types are erased at runtime, and what follows from that?

  20. 20What is the difference between any and unknown in TypeScript?Intermediate

    any turns off type checking for a value: you can call it, index it or assign it anywhere without errors, and the unsafety spreads to everything it touches. unknown means 'a value of a type I do not know yet'; you can assign anything to it, but must narrow it with a check such as typeof, instanceof or a validator before using it.

    Use unknown for data from outside your control, such as parsed JSON, catch variables or API responses, and validate it, ideally with a schema library, before trusting its shape. Reserve any for gradual migration, and prefer a narrow cast with a comment over a blanket one. Enabling strict and noImplicitAny stops accidental any.

    What the interviewer is checking: Whether you treat type safety at boundaries as a habit, not a formality.

    Likely follow-up: How do you type the catch (e) variable safely?

  21. 21What are generics in TypeScript? Show a case where a constraint like K extends keyof T helps.Advanced

    Generics let a function or type work over many types while keeping the relationship between inputs and outputs. first<T>(xs: T[]): T | undefined returns a number for a number array and a string for a string array, which any could not express.

    Constraints narrow what a type parameter may be. In pick<T, K extends keyof T>(o: T, k: K): T[K], K must be a real key of T, so a typo is a compile error and the return type is exactly that property's type. Generics are also used for reusable containers, API response wrappers and typed hooks. Avoid generic parameters that appear only once; they usually add noise, not safety.

    typescript
    function first<T>(xs: readonly T[]): T | undefined {
      return xs[0];
    }
    function pick<T, K extends keyof T>(o: T, k: K): T[K] {
      return o[k];
    }
    console.log(first([1, 2]), first(["a"]), first([]));
    console.log(pick({ id: 1, name: "x" }, "name"));   // pick(..., "nope") is a compile error

    Output

    1 a undefined
    x

    What the interviewer is checking: Understanding generics as relationships between types, plus constraints and keyof.

    Likely follow-up: What is the difference between a generic constraint and a default type parameter?

  22. 22How do discriminated unions and exhaustive checks make TypeScript code safer?Advanced

    A discriminated union is a union of object types that share a literal-typed tag, such as kind: "circle" | "rect". When you check the tag in an if or switch, the compiler narrows the variable to the matching member, so you can access that member's fields safely.

    For exhaustiveness, assign the leftover value to a variable of type never in the default branch. If someone adds a new member to the union later, every switch that forgot to handle it stops compiling. This models states like loading, success and error in UI code far better than a bag of optional fields, and removes impossible combinations.

    typescript
    type Shape =
      | { kind: "circle"; r: number }
      | { kind: "rect"; w: number; h: number };
    
    function area(s: Shape): number {
      switch (s.kind) {
        case "circle": return Math.round(Math.PI * s.r ** 2);
        case "rect": return s.w * s.h;
        default: {
          const _never: never = s;   // compile error if a case is missing
          return _never;
        }
      }
    }
    console.log(area({ kind: "rect", w: 2, h: 3 }), area({ kind: "circle", r: 2 }));

    Output

    6 13

    What the interviewer is checking: Using the type system to model states and make illegal states unrepresentable.

    Likely follow-up: Why is this better than a single interface with optional fields?

  23. 23In Node.js, what is the order of process.nextTick, promise callbacks and setTimeout?Advanced

    After the current synchronous code finishes, Node drains the process.nextTick queue first, then the promise microtask queue, and only then moves on to the event loop phases where timers and I/O callbacks run. So nextTick callbacks come before promise callbacks, which come before a setTimeout(..., 0).

    Both queues are drained fully before the loop continues, so a callback that keeps scheduling more nextTicks or microtasks can starve timers and I/O. Prefer promises or queueMicrotask for most deferral, and setImmediate when you want to yield to the I/O phase. The relative order of setTimeout(0) and setImmediate from the main module is not guaranteed.

    What the interviewer is checking: Node-specific event-loop depth and awareness of starvation risks.

    Likely follow-up: How would you process a huge array without blocking the loop?

  24. 24You need to call an API for 1,000 items but with at most 5 requests in flight. How do you write that?Advanced

    Firing all 1,000 with Promise.all overwhelms the server or hits rate limits, and awaiting one by one is needlessly slow. Use a small worker pool: start N lanes that each pull the next index from a shared counter, await the job, and loop until the list is empty. Then await Promise.all on the lanes.

    Writing results by index preserves input order. Decide the failure policy up front: a rejection in one lane makes Promise.all reject, while retries or collecting per-item errors need a try/catch around each job. Add a retry with backoff for transient failures. In production you might use a tested library such as p-limit instead of rolling your own.

    javascript
    async function mapLimit(items, limit, worker) {
      const results = new Array(items.length);
      let next = 0;
      async function lane() {
        while (next < items.length) {
          const i = next++;
          results[i] = await worker(items[i]);
        }
      }
      const lanes = Array.from({ length: Math.min(limit, items.length) }, lane);
      await Promise.all(lanes);
      return results;
    }
    
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    mapLimit([1, 2, 3, 4, 5, 6], 2, async (n) => {
      await sleep(10 * n);
      return n * n;
    }).then(console.log);

    Output

    [ 1, 4, 9, 16, 25, 36 ]

    What the interviewer is checking: Designing bounded concurrency from first principles and thinking about ordering and failures.

    Likely follow-up: How would you make one failed item not abort the others?

  25. 25A search box fires a request per keystroke and old responses sometimes overwrite newer ones. How do you fix it?Advanced

    This is a race condition: responses can arrive out of order, so the last response to arrive wins instead of the response to the latest query. The fix is to ensure only the most recent request is allowed to update the UI.

    Cancel stale requests with an AbortController (pass its signal to fetch, abort the previous one on each new query), or tag each request with an incrementing id and ignore results whose id is no longer the latest. Combine this with a debounce to send fewer requests. In React, do the same cleanup in the effect's return function so an unmounted component does not set state.

    javascript
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    let latest = 0;
    
    async function search(term, delay) {
      const id = ++latest;
      await sleep(delay);              // stand-in for fetch()
      if (id !== latest) return console.log("ignored stale result for", term);
      console.log("render results for", term);
    }
    
    search("pi", 50);      // slow response
    search("pizza", 10);   // newer, faster response

    Output

    render results for pizza
    ignored stale result for pi

    What the interviewer is checking: Recognising async race conditions and knowing cancellation and ordering strategies.

    Likely follow-up: What is the difference between aborting a request and ignoring its result?

  26. 26Where do memory leaks come from in JavaScript apps, and how would you find one?Advanced

    The garbage collector frees objects that are unreachable, so a leak means something still references data you no longer need. Common causes are event listeners or subscriptions never removed, timers (setInterval) never cleared, caches or maps that grow without bound, closures holding large objects, and detached DOM nodes still referenced from JavaScript.

    To find one, compare heap snapshots in the browser's DevTools (or Node's inspector) taken before and after repeating an action, and look at what is growing and what retains it. Fixes: remove listeners and clear timers on teardown, bound your caches, and use WeakMap or WeakRef when a mapping should not keep the key alive.

    What the interviewer is checking: Practical diagnosis skills: the usual retention paths and the tooling to spot them.

    Likely follow-up: When would you reach for WeakMap instead of Map?

Fill the gaps with a free course

If one of these JavaScript answers felt shaky, these 2 courses cover the same ground step by step: JavaScript Fundamentals, TypeScript.

More interview questions

Practising JavaScript for a real opening? Upload your resume to see which open jobs list JavaScript skills you already have, or browse open roles.