~/tutorials/javascript-interview-questions-in-2026-21-a-senior-actually-asks
§ POST · MAY 14, 2026 v1.0

JavaScript interview questions in 2026: 21 a senior actually asks

21 JavaScript interview questions a senior dev actually asks in 2026: closures, event loop, hoisting, promises, this binding, with the answers and follow-ups.
Ryan CallowayStaff contributor
  11 min read

By Ryan Calloway. Updated May 2026.

Lydia Hallie’s “JavaScript questions” repo has crossed 65k stars by curating questions that actually appear on screens, not trivia. The shape changed again in 2024-2025: AI handles “reverse a string” in 200 ms, so the questions that survive are “read this code, find the bug, explain it” plus a handful that test the language internals. The 2025 Stack Overflow Survey put TypeScript ahead of JavaScript in new GitHub repo creation in late 2025, and React 19 is now in front of nearly half of daily React users; both shifts show up in question content. This is the 21-question set a 2026 senior screen draws from, with the answer the interviewer is listening for and the follow-up they will probe if you do not say it first.

Quick answer: the questions cluster in five groups: language fundamentals (event loop, this, closures, hoisting), async (Promise primitives, AbortSignal, top-level await), modern JS (ES2024 additions: Object.groupBy, Set methods, iterator helpers, Promise.try), classic implementation challenges (debounce, deep clone, memoize, pub/sub), and the 2026-specific block (TypeScript design, React 19 use hook + Actions, AI-tool workflow, edge runtime). Twenty-one is the rough top of one loop; you do not need to ace every one, but you do need to recognize the shape.

What to expect in the loop

Three rounds is standard: a screen with 4-5 fundamentals, a 45-90 minute coding pair (live, screen-shared, AI tools sometimes allowed and observed), and a system or framework-specific round. The 2024-2025 r/cscareerquestions threads on “interview format changed” all point at the same shift: take-homes still happen, but most loops add a live screen-shared round because too many take-homes were being submitted by candidates who could not explain the code in follow-ups.

Fundamentals (questions 1-5)

1. Explain the event loop in 90 seconds

The single most-asked question. Three claims to land, ideally without prompting:

  1. JavaScript runs on a single thread; one call-stack frame at a time.
  2. Callbacks queue on two queues: microtasks (Promises, queueMicrotask) and macrotasks (setTimeout, I/O). The loop drains microtasks fully between each macrotask.
  3. That is why Promise.resolve().then(fn) fires before setTimeout(fn, 0).

Drawing the stack + two queues on a virtual whiteboard is a pass. The event loop guide has the full model.

2. What does this print?

for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// 0 1 2

var is function-scoped — three callbacks share one i. let is block-scoped — each iteration creates a fresh binding the closure captures. “let is ES6″ is not the answer; the scope model is.

3. What is this in each call?

const obj = {
  name: "A",
  regular() { return this.name; },
  arrow: () => this.name,
};
obj.regular();          // "A"
obj.arrow();            // undefined (this from enclosing scope)
const loose = obj.regular;
loose();                // undefined in strict mode

Four binding rules in precedence order: new, explicit (call/apply/bind), method (obj.fn()), default (undefined in strict, globalThis in sloppy). Arrow functions skip all four; they inherit this from the enclosing scope. The MDN this reference is canonical.

4. Closures, with one real use case

function makeCounter() {
  let count = 0;
  return { inc: () => ++count, value: () => count };
}

A closure is a function bundled with the scope chain it was created in. Strong answers point at a real reason it matters — privacy, module-state, memoization caches — and a concrete example. “Functions that remember variables” is correct but vague.

5. Hoisting and the temporal dead zone

console.log(a); // undefined (var declaration hoisted, value not)
console.log(b); // ReferenceError (TDZ)
var a = 1;
let b = 2;

var declarations are hoisted with an initial undefined. let and const are hoisted but in the temporal dead zone until evaluation reaches the declaration. Function declarations hoist with their body; function expressions follow the binding rule of var/let/const.

Async and concurrency (questions 6-9)

6. Implement Promise.all from scratch

function all<T>(promises: Promise<T>[]): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const out: T[] = new Array(promises.length);
    let remaining = promises.length;
    if (remaining === 0) return resolve(out);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        (v) => { out[i] = v; if (--remaining === 0) resolve(out); },
        reject
      );
    });
  });
}

Three traps: empty array (resolve to [] immediately), preserving order via out[i] not out.push(), and short-circuit on first rejection. The ECMA-262 spec covers the exact semantics.

7. Predict the output

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

Synchronous code first (1, 5), then microtasks in FIFO (3, 4), then the next macrotask (2). If the candidate can verbalize the queue draining order, they understand the loop.

8. AbortController and AbortSignal.timeout

// Modern timeout (Node 17.3+, all evergreen browsers)
const r = await fetch(url, { signal: AbortSignal.timeout(2000) });

// Compose user cancel + timeout
const ctrl = new AbortController();
const r = await fetch(url, {
  signal: AbortSignal.any([ctrl.signal, AbortSignal.timeout(5000)]),
});

Hand-rolled Promise.race timeouts orphan the underlying request. AbortSignal.timeout actually cancels it. AbortSignal.any composes signals. Saying “I’d Promise.race a timeout” is a 2019 answer.

9. Promise.try and Promise.withResolvers (ES2024)

// Lift a maybe-sync function uniformly into a promise
Promise.try(maybeSyncFn).then(use).catch(handle);

// Resolve from outside the executor
const { promise, resolve, reject } = Promise.withResolvers();
emitter.once("done", resolve);
return promise;

Both shipped in 2024 and reached Baseline in 2025; both are in Node 22 LTS. A senior who has read release notes mentions one or both. See async/await vs Promises for the full coverage.

Modern JS (questions 10-12)

10. Object.groupBy and Set methods (ES2024)

// Group an array of objects by a key — was a 6-line reduce, now a one-liner
const byRole = Object.groupBy(users, (u) => u.role);
// { admin: [...], member: [...], guest: [...] }

// Set methods
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.intersection(b);     // Set { 2, 3 }
a.union(b);            // Set { 1, 2, 3, 4 }
a.difference(b);       // Set { 1 }

Object.groupBy is in every evergreen browser since 2024 and Node 21+. The Set methods (MDN reference) reached Baseline in 2025. Senior candidates know the modern stdlib; juniors hand-roll the equivalents.

11. Iterator helpers

// Lazy: never builds a 1M-element array
const firstEvens = numbers()
  .filter((n) => n % 2 === 0)
  .take(10)
  .toArray();

Iterator helper methods (map, filter, take, drop, flatMap, reduce, toArray) are now Baseline in 2025 and ship in Node 22+. They are lazy where Array.prototype.map is eager, which matters for streaming and infinite generators.

12. Top-level await trade-offs

Available in ES modules in Node 14.8+ and every evergreen browser. It blocks module evaluation until resolved. Fits boot-time config loaders. Avoids belong in modules pulled into hot paths — every consumer pays the latency on first import, which appears in serverless cold-start metrics. Node 22 makes ESM the default; the v22 release notes have the migration details.

Implementation patterns (questions 13-16)

13. Debounce in 10 lines, typed

function debounce<F extends (...args: any[]) => any>(fn: F, ms: number): F {
  let t: ReturnType<typeof setTimeout> | null = null;
  return ((...args: Parameters<F>) => {
    if (t) clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  }) as F;
}

What separates strong answers: clearing the previous timer, preserving (or knowingly skipping) this, and typing the wrapper with Parameters<F>/ReturnType instead of any. Throttle is the same pattern with a “last-fired” timestamp instead of a clear.

14. Deep clone without JSON round-trip

// 2026 answer
const copy = structuredClone(originalObject);

structuredClone handles cycles, Date, Map, Set, RegExp, typed arrays. The MDN reference lists supported types. The interviewer wants both: the manual recursive version (showing you understand the algorithm) and the one-line stdlib answer (showing you know the modern primitive). The JSON trick is wrong because it drops functions, Date, Map, Set, RegExp, and breaks on cycles.

15. Memoize a pure function

function memoize<Args extends unknown[], R>(fn: (...args: Args) => R) {
  const cache = new Map<string, R>();
  return (...args: Args): R => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn(...args));
    return cache.get(key)!;
  };
}

Traps: JSON.stringify as key is fast but breaks on functions and cycles; Map beats a plain object for non-string keys; production needs an LRU bound (see lru-cache) to avoid unbounded memory growth.

16. Type-safe pub/sub

class Emitter<Events extends Record<string, unknown>> {
  private listeners: { [E in keyof Events]?: Set<(p: Events[E]) => void> } = {};

  on<E extends keyof Events>(event: E, handler: (p: Events[E]) => void) {
    (this.listeners[event] ??= new Set()).add(handler);
    return () => this.listeners[event]?.delete(handler);
  }

  emit<E extends keyof Events>(event: E, payload: Events[E]) {
    this.listeners[event]?.forEach((fn) => fn(payload));
  }
}

const bus = new Emitter<{ login: { userId: number }; logout: void }>();
bus.on("login", (p) => console.log(p.userId));   // typed
bus.emit("logn", { userId: 1 });                  // compile error

Strong signal: typed Record<string, unknown> events so misspellings are compile errors, returning the unsubscribe function from on, and using Set for handlers (cheap deletion). See TypeScript generics explained for the patterns.

TypeScript design (questions 17-18)

17. satisfies vs as

type Theme = "light" | "dark";
const t1 = "light" as Theme;          // type widens to Theme
const t2 = "light" satisfies Theme;   // type stays "light"

const config = {
  primary: "#000",
  secondary: "#fff",
} satisfies Record<string, string>;   // validated, not widened
config.primary;                        // type is "#000" (literal)

as weakens the type to whatever you cast to and silences errors. satisfies validates the value against a type without widening. Always prefer satisfies when both fit; reach for as only when you have type information the compiler cannot derive.

18. --noUncheckedIndexedAccess

const arr: string[] = ["a", "b"];
arr[0].toUpperCase();   // off: ok, but crashes when arr is empty at runtime
                        // on: error, type is string | undefined

const config: Record<string, string> = {};
config.key.length;      // off: ok; on: must check for undefined first

Off by default in --strict. With it on, indexed access is T | undefined instead of T; you handle the empty case before crashing. Senior candidates explicitly mention it because they know the bug class it eliminates.

React 19 (questions 19-20)

19. The use hook and Actions

// Reading a Promise without useEffect
function User({ promise }: { promise: Promise<User> }) {
  const user = use(promise);   // suspends until resolved
  return <div>{user.name}</div>;
}

// An Action: form handler with built-in pending/error state
function ProfileForm() {
  const [state, action, isPending] = useActionState(updateProfile, initial);
  return <form action={action}>{/* ... */}</form>;
}

The use hook (React docs) reads a Promise or context conditionally — both of which were impossible with useEffect + useState. useActionState handles form submission state without writing the loading flag yourself. React 19 became stable in December 2024; senior candidates have used both in production by mid-2025.

20. Server Components vs Client Components

Server Components render on the server, ship zero client JS, can await data directly, and cannot use hooks or browser APIs. Client Components render on both server and client, can use hooks, and ship the JS. The boundary is the "use client" directive. The follow-up: which logic to put on which side. Answer: data fetching and templating live in server components; interaction lives in client components; the hard part is the data shape passed across the boundary, which must be serializable.

System and AI fluency (question 21)

21. “Walk me through how you use AI tools in your workflow”

The 2025 Stack Overflow Survey put 84% of developers using AI tools and trust in their output at 29%. The question is no longer “do you use AI” — it is “how, with what guardrails.” A passing answer covers four points:

Be ready for the follow-up “design a small system that uses an LLM as a component” — common at startups in 2026, asks you to talk about prompt boundaries, structured outputs, retries, cost, and where to put the human.

What does not separate signal from noise anymore

FAQ

Which topics should I brush up on first?

Event loop, closures, this, Promises, prototypes — those five dominate the fundamentals round. The recurring r/learnjavascript “what does this print” threads converge on the same five every quarter. The event loop guide and async/await vs Promises cover the highest-value pair.

Do I need TypeScript for a JavaScript interview in 2026?

For mid-level or senior at a tech-forward company, yes. You do not need to write advanced generics, but you should be comfortable typing function signatures and explaining strictNullChecks and --noUncheckedIndexedAccess. See TypeScript generics explained.

Do I need to know React 19 for a “JavaScript” interview?

If the role mentions React, yes — use, useActionState, Server Components, Suspense, and the rendering boundaries. The recurring 2025-2026 r/reactjs “interview asked me about” threads converge on those five. The useEffect cleanup guide covers the lifecycle subtleties that show up alongside.

Are AI-generated answers disqualifying in screens?

If you cannot explain the code on a follow-up, yes. Most loops added a live screen-shared coding round in 2024-2025 specifically because too many take-homes were submitted by candidates who could not explain them. Use AI to prepare; use it during a live coding round only if it is explicitly allowed and you can keep up with the explanation.

What should I avoid on the coding portion?

Pretending. If you do not know an API, say so; reach for what you do know (docs, a pseudo-import) and solve the problem around it. Confident guessing reads as worse than admitting unfamiliarity in every public engineering hiring debrief I have read.

Sources and further reading

Next steps

For Promise-typed systems, see async/await vs Promises. For the type-system portion of any 2026 senior loop, the TypeScript generics guide is the next read. For the career strategy that gets you the loop in the first place, how to get your first developer job in 2026 covers the application side.

esc