JavaScript & TypeScript Interview Guide: From Closures to the Event Loop
Master Scope, Prototypes, Asynchronous Execution & TypeScript's Structural Type System for Frontend and Full-Stack Rounds

What You Must Master to Clear This Track
- Closures are not a trick question topic — they underpin module patterns, memoization, and private state in every production codebase.
- The event loop's rule of 'drain all microtasks before the next macrotask' explains almost every async ordering question you'll be asked.
- Prototypal inheritance is the real mechanism behind `class` syntax; interviewers probe whether you know what's happening beneath the sugar.
- TypeScript's type system is structural, not nominal — two differently-named types with the same shape are interchangeable.
- Most 'JavaScript is weird' interview gotchas (coercion, `this`, equality) have a small, learnable rule set behind them.
Step-by-Step Study Plan
Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.
Scope, Hoisting, Closures & Prototypes
Build a rock-solid mental model of how JavaScript resolves variables, how functions capture their surrounding scope, and how objects inherit behavior through the prototype chain.
- •Trace var/let/const hoisting and the temporal dead zone for at least 10 code snippets without running them.
- •Implement a private-state module (counter, cache, or event emitter) using only closures, no classes.
- •Diagram the prototype chain for a custom constructor function and for an ES6 class extending it.
- •Practice reading code snippets aloud and predicting output before pasting into a console.
- •Rebuild `Object.create` and a minimal class-inheritance helper from scratch to internalize the mechanism.
Call Stack, Microtasks, Macrotasks & Promises
Master the single-threaded concurrency model: how the call stack, microtask queue, and macrotask (task) queue interact, and how async/await composes on top of Promises.
- •Correctly predict console.log ordering for mixed sync code, Promise chains, and setTimeout calls.
- •Rewrite a callback-based function into a Promise-based one, then into async/await.
- •Handle rejected Promises correctly with try/catch, .catch(), and global unhandledrejection listeners.
- •Draw the event loop diagram from memory: call stack, Web/Node APIs, microtask queue, macrotask queue.
- •Debug at least 3 real 'race condition' bugs involving async/await inside loops.
Generics, Utility Types, Structural Typing & Runtime Pitfalls
Move from 'TypeScript as annotations' to 'TypeScript as a design tool' — generics, conditional types, discriminated unions — while shoring up classic runtime pitfalls like `this` binding and coercion.
- •Write a generic function and a generic interface that both narrow correctly at call sites.
- •Use Partial, Pick, Omit, and Record to reshape an existing interface without duplicating fields.
- •Explain 4 distinct `this`-binding scenarios (default, implicit, explicit, arrow) with a live code example each.
- •Convert 5 `any`-typed functions from a real project into properly generic, type-safe signatures.
- •Run 10 timed mock questions on coercion and equality (`==` vs `===`, `[] + []`, `NaN === NaN`).
1. Closures, Scope & Hoisting
Closures are the single most tested JavaScript concept because they explain module encapsulation, event handler bugs, and memoization in one mechanism: a function remembers the scope it was created in, not the scope it is called from.
Scope is determined by where a function is physically written in the source code, not by where or how it is invoked. Nested functions can read (and close over) variables from their enclosing function.
var declarations are hoisted and initialized to undefined at the top of their function scope; let/const are hoisted but left uninitialized in the Temporal Dead Zone (TDZ) until their declaration line executes, throwing a ReferenceError if accessed early.
A closure lets an inner function retain access to outer variables even after the outer function has returned, enabling private state without classes (the classic 'module pattern').
A var-declared loop variable is shared across all iterations because var is function-scoped, so callbacks scheduled inside the loop (e.g. setTimeout) all see the final value. let creates a fresh binding per iteration, fixing this by design.
function createCounter(initialValue: number = 0) {
let count = initialValue; // captured by closure, invisible outside
return {
increment: () => (count += 1),
decrement: () => (count -= 1),
getValue: () => count,
};
}
const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.getValue()); // 12
// counter.count is undefined — there is no public property to read;
// the only way to touch `count` is through the closures returned above.- When asked to fix the 'var in a loop + setTimeout' bug, mention THREE valid fixes: switch to let, wrap the body in an IIFE that captures the value, or pass the value as a bind() argument.
- Say 'lexical scope' and 'Temporal Dead Zone' explicitly in interviews — these are the exact terms interviewers listen for.
- Assuming hoisting means the whole declaration AND assignment move to the top — only the declaration is hoisted, the assignment stays in place.
- Forgetting that closures capture variables by reference, not by value — mutating a captured variable later changes what every closure over it sees.
2. Prototypes & the Prototype Chain
Every JavaScript object has an internal link to another object called its prototype. Property lookups walk this chain until the property is found or the chain ends at null — this is the real mechanism behind inheritance, even when you write `class` and `extends`.
A function's `.prototype` property is the object that will become the [[Prototype]] of instances created with `new`. An object's `__proto__` (or Object.getPrototypeOf) is the actual live link used during property lookup.
Accessing obj.foo first checks obj's own properties; if missing, the engine walks up obj.[[Prototype]], then that object's [[Prototype]], and so on until it hits Object.prototype and finally null.
ES6 class syntax still creates a constructor function under the hood; methods defined in the class body are placed on ClassName.prototype, and `extends` wires up the prototype chain via Object.setPrototypeOf.
Object.create(proto) creates a new object whose [[Prototype]] is set directly to `proto`, letting you build inheritance hierarchies without ever calling a constructor function.
- If asked to implement inheritance without `class`, reach for Object.create(parent.prototype) and manually fix the .constructor reference — this signals you understand what `extends` does for you.
- Mention shadowing: assigning obj.foo = x creates an OWN property that shadows a prototype property of the same name rather than mutating the prototype.
- Confusing `instanceof` (checks the prototype chain) with `typeof` (checks the primitive/type tag) — they answer different questions.
- Directly mutating Object.prototype or Array.prototype ('monkey-patching' or 'prototype pollution'), which silently affects every object/array in the program and is a real security vulnerability when attacker-controlled data reaches a merge/clone utility.
3. The Event Loop: Call Stack, Microtasks & Macrotasks
JavaScript is single-threaded, so asynchronous behavior — timers, network requests, Promise resolution — is all coordinated by the event loop, which decides what runs next once the call stack is empty. Interview questions almost always test whether you know microtasks always drain before the next macrotask.
Synchronous code executes frame-by-frame on a single call stack. Nothing asynchronous can run until the stack is completely empty, which is why a long synchronous loop blocks timers and UI rendering.
Promise .then/.catch/.finally callbacks, async/await continuations, and queueMicrotask() callbacks all go here. The ENTIRE microtask queue is drained — including new microtasks queued while draining — before the loop touches the next macrotask.
setTimeout, setInterval, setImmediate (Node), I/O callbacks, and UI events are macrotasks. The event loop runs exactly one macrotask per turn, then drains microtasks again before the next one.
An async function always returns a Promise; `await` pauses the function and schedules its continuation as a microtask once the awaited Promise settles — it does not block the thread.
How the runtime interleaves synchronous code, microtasks, and macrotasks on every turn of the loop.
- When tracing output order, label every line with sync / microtask / macrotask before writing the final answer — interviewers grade the reasoning, not just the final order.
- Mention that `await Promise.resolve()` yields exactly one microtask tick, which is a common building block for 'flush the microtask queue' test utilities.
- Assuming setTimeout(fn, 0) runs immediately after the current synchronous code — it always runs AFTER all pending microtasks, even ones queued after the timer was set.
- Writing an unbounded recursive Promise chain that keeps re-queuing microtasks, starving macrotasks (and the UI) from ever running.
4. TypeScript's Type System & Common Runtime Pitfalls
TypeScript adds a structural, compile-time type system on top of JavaScript's dynamic runtime. Interviewers test whether you can use generics and utility types to model real APIs, and whether you understand the runtime quirks (this binding, coercion) that TypeScript's types cannot fully protect you from.
TypeScript compares the SHAPE of types, not their declared names — two unrelated interfaces with identical members are freely assignable to each other, unlike nominal type systems such as Java or C#.
Generics let a function, interface, or class stay type-safe while remaining reusable across many concrete types, replacing unsafe `any` with a type parameter that TypeScript infers and checks at every call site.
Partial<T>, Required<T>, Pick<T,K>, Omit<T,K>, and Record<K,V> transform existing types instead of duplicating field lists, keeping a single source of truth for shared domain models.
Regular functions determine `this` from the CALL SITE (default, implicit object, or explicit call/apply/bind), while arrow functions have no `this` of their own and lexically inherit it from the enclosing scope — the source of most 'this is undefined' bugs.
Operators like + and == trigger the abstract ToPrimitive/ToNumber conversion algorithm, producing famously surprising results (e.g. '5' + 3 === '53' but '5' - 3 === 2) that TypeScript's static types do not eliminate at runtime boundaries like JSON parsing or form inputs.
interface ApiResponse<T> {
data: T;
status: "success" | "error";
timestamp: number;
}
// Conditional type with `infer`: pull the T back out of an ApiResponse<T>
type ExtractData<R> = R extends ApiResponse<infer D> ? D : never;
interface User {
id: string;
name: string;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch("/api/users/" + id);
const data: User = await res.json();
return { data, status: "success", timestamp: Date.now() };
}
// Compose built-in utility types with our own conditional type:
type UserShape = ExtractData<Awaited<ReturnType<typeof fetchUser>>>; // resolves to User
// Reshape User without retyping fields:
type UserPreview = Pick<User, "id" | "name">;
type PartialUser = Partial<User>;- When explaining interface vs type alias, lead with what they share (both describe object shapes, both support generics) before the differences (only interface supports declaration merging; only type alias can name unions/primitives directly).
- For `this` questions, always state the four binding rules in priority order: new binding > explicit (call/apply/bind) > implicit (obj.method()) > default (undefined in strict mode / global object otherwise) — arrow functions opt out of this system entirely.
- Using `any` to silence a type error instead of modeling the real shape with a generic or a union — this reintroduces the exact runtime bugs TypeScript exists to prevent.
- Passing an object method as a bare callback (e.g. setTimeout(obj.method, 1000)) and being surprised `this` is undefined inside it — the function is detached from `obj` at the call site.
- Relying on `==` for equality checks and getting tripped up by coercion edge cases like '' == 0 or null == undefined being true while null == 0 is false.
Fixing a Production Race Condition in a Checkout Flow
A mid-size e-commerce team noticed that roughly 2% of checkout submissions occasionally applied a stale discount code from a previous cart session, even though the UI showed the correct code. The bug only reproduced intermittently in production, never locally.
- 1The team traced the bug to an async function that fetched cart totals and discount validity inside a loop over cart items, awaiting each network call sequentially instead of in parallel.
- 2A closure over a shared `currentCartId` variable (declared with `let` at module scope, not per-request) was being read by a callback that resolved AFTER the user had already navigated to a new cart.
- 3They refactored the sequential awaits into a `Promise.all` batch keyed by a request-scoped cart ID passed explicitly as a function argument, removing the shared mutable closure variable entirely.
- 4They added a discriminated union (`{status: 'stale'} | {status: 'fresh', data}`) return type so any consumer was forced by TypeScript to handle the stale case instead of assuming the response always matched the current cart.
- 5They wrote a regression test using fake timers to simulate two overlapping cart sessions and assert the stale response was discarded.
- 6The fix was rolled out behind a feature flag to 10% of checkout traffic for one week before a full rollout.
Top Must-Know Interview Questions & Model Answers
Q1: What is a closure, and what is a practical production use case for one?
- •Closures power the module pattern: a factory function returns an object of methods that all share one private, non-exported variable.
- •They are also the mechanism behind memoization caches, debounce/throttle utilities, and React's useState (each render's closure captures that render's state).
Q2: Explain hoisting and the difference between how var, let, and const are hoisted.
- •Reading a let/const variable before its declaration throws a ReferenceError ('Cannot access before initialization'), whereas reading a hoisted var simply yields undefined.
- •Function declarations are hoisted with their full body, so they can be called before their textual position; function expressions and arrow functions follow the variable hoisting rules of var/let/const.
Q3: Why does this loop print 5 five times instead of 0,1,2,3,4, and how do you fix it? for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); }
- •Fix 1: replace var with let — let creates a fresh binding of i for each loop iteration, so each closure captures its own copy.
- •Fix 2: wrap the loop body in an IIFE that takes i as a parameter, creating a new scope per iteration.
- •Fix 3: pass i as an extra argument to setTimeout (setTimeout(fn, delay, i)), which forwards it as an argument rather than relying on closure capture.
Q4: What is the difference between lexical scope and the scope chain?
- •The scope chain always starts at the innermost function and walks outward to the module/global scope — it never depends on the call stack or who invoked the function.
- •This is why JavaScript is described as lexically (statically) scoped rather than dynamically scoped like some shell languages.
Q5: How would you implement a simple memoization utility using closures?
- •The returned wrapper function closes over the cache variable, so the cache persists across calls without being exposed globally.
- •For multi-argument functions, the cache key is usually a serialized (e.g. JSON.stringify) form of the arguments, with awareness that this breaks down for non-serializable arguments like functions or circular objects.
Q6: What is the prototype chain, and how does the JavaScript engine resolve obj.someProperty?
- •This lookup happens on every property/method access, which is why adding a method to Array.prototype makes it instantly available on every existing array.
- •The chain always terminates at Object.prototype (whose own [[Prototype]] is null), unless the object was created with Object.create(null), which has no prototype at all.
Q7: What is the difference between a function's .prototype property and an object's __proto__?
- •Calling `new Foo()` sets the new object's [[Prototype]] to Foo.prototype at that moment — later reassigning Foo.prototype does not retroactively change already-created instances.
- •__proto__ is a legacy accessor for [[Prototype]]; the standard way to read/write it is Object.getPrototypeOf() / Object.setPrototypeOf().
Q8: How does ES6 `class` and `extends` map onto prototypal inheritance under the hood?
- •`super(...)` inside a subclass constructor calls the parent constructor with the correct `this`, which is mandatory to run before accessing `this` in a derived class.
- •Static methods are copied onto the constructor function itself (not .prototype), and `extends` also links the subclass constructor's own [[Prototype]] to the superclass constructor for static inheritance.
Q9: What is prototype pollution and why is it treated as a security vulnerability?
- •A naive recursive merge/clone function that copies keys without checking for '__proto__', 'constructor', or 'prototype' can be tricked into writing to the global Object.prototype.
- •The fix is to use Object.create(null) for dictionary-like objects, use Map instead of plain objects for untrusted keyed data, and explicitly block dangerous keys in merge utilities (or use a vetted library that already does).
Q10: Describe the JavaScript event loop: what are the call stack, the microtask queue, and the macrotask queue?
- •Microtasks always have priority: even if a macrotask is ready, the loop will not touch it until every currently queued microtask (including ones enqueued while draining) has run.
- •This two-queue model is why Promise-based code appears to run 'sooner' than setTimeout-based code, even with a 0ms delay.
Q11: Why does a Promise's .then() callback run before a setTimeout(fn, 0) callback, even though both were scheduled at the same time?
- •Even setTimeout(fn, 0) does not run 'immediately' — it is placed on the macrotask queue and must wait for the current synchronous script AND all pending microtasks to finish first.
- •This ordering is guaranteed by the spec, not an implementation detail, so it is safe to rely on in interviews and in production code.
Q12: Trace the console.log output order for: console.log(1); setTimeout(() => console.log(2)); Promise.resolve().then(() => console.log(3)); console.log(4);
- •1 and 4 run synchronously as the script executes top to bottom, both before the stack empties.
- •Once the stack is empty, the microtask queue (the Promise .then) is drained, printing 3, and only then does the event loop pull the setTimeout callback off the macrotask queue, printing 2.
Q13: What are the different sources of microtasks versus macrotasks?
- •In the browser, macrotasks also include things like requestAnimationFrame-adjacent rendering steps and dispatched DOM events; in Node.js, process.nextTick runs even before microtasks in its own even-higher-priority queue.
- •Knowing which bucket a given API falls into is the fastest way to predict execution order without running the code.
Q14: How does async/await relate to Promises, and what actually happens when you `await` a value?
- •Under the hood, async/await is largely sugar over generator functions plus a driver that automatically calls .next() when the yielded Promise resolves.
- •Awaiting a non-Promise value still yields control and resumes on a microtask (the value is implicitly wrapped via Promise.resolve()), so even `await 1;` introduces a tick of asynchrony.
Q15: What happens to an unhandled rejected Promise, and how should you guard against it?
- •Wrap awaited calls in try/catch inside async functions, or attach .catch() to Promise chains, rather than assuming errors will surface elsewhere.
- •For centralized handling, register a global 'unhandledrejection' (browser) or 'unhandledRejection' (Node) listener as a last-resort safety net, not as the primary error-handling strategy.
Q16: How does Promise.all differ from Promise.allSettled and Promise.race?
- •Promise.all is best when you need every result and a single failure should abort the whole batch (e.g. all required API calls).
- •Promise.allSettled is best for independent operations where partial failure is acceptable (e.g. best-effort logging to multiple services); Promise.race is used for timeouts (racing a real request against a timer Promise).
Q17: Why is running `await` sequentially inside a for-loop often a performance bug, and how do you fix it?
- •The fix for independent operations is to start all the Promises first (e.g. map to an array of Promises) and then await them together with Promise.all, letting them run concurrently.
- •Sequential awaiting is still correct (and necessary) when each iteration genuinely depends on the previous result, so the fix only applies when the operations are truly independent.
Q18: What are generics in TypeScript, and why are they preferable to typing a parameter as `any`?
- •A function like `function identity<T>(x: T): T` returns exactly the type passed in, so callers keep full type information on the result, unlike `any` which infects everything downstream with no checking.
- •Generic constraints (`<T extends { id: string }>`) let you require a minimal shape while still preserving the caller's more specific type.
Q19: Explain structural typing in TypeScript and how it differs from the nominal typing used in languages like Java.
- •This means an object literal satisfying an interface's required members is assignable to that interface even if it was never declared to implement it — often called 'duck typing'.
- •Excess property checks are a partial exception: TypeScript flags object LITERALS with extra properties assigned directly to a typed variable, even though structurally a superset would otherwise be assignable.
Q20: What do the Partial<T>, Pick<T, K>, Omit<T, K>, and Record<K, V> utility types do?
- •These are all built with mapped types (`{ [P in keyof T]?: T[P] }` style) under the hood, which is worth mentioning to show you understand the mechanism, not just the names.
- •Combining them is common in real code, e.g. `Partial<Pick<User, 'name' | 'email'>>` for a PATCH endpoint's request body type.
Q21: What is the difference between an `interface` and a `type` alias in TypeScript?
- •Declaration merging makes `interface` the right choice for augmenting third-party or ambient types (e.g. extending the Express Request object).
- •`type` is generally preferred for unions, mapped/conditional types, and anywhere you're computing a shape rather than declaring a fixed contract.
Q22: What are conditional types and the `infer` keyword used for in TypeScript?
- •Built-ins like `ReturnType<T>` and `Awaited<T>` are implemented using exactly this pattern: `T extends (...args: any[]) => infer R ? R : never`.
- •Conditional types distribute over union types by default (`T extends U ? X : Y` applied to `A | B` becomes `(A extends U ? X : Y) | (B extends U ? X : Y)`), which is a common source of confusion in interviews.
Q23: What are discriminated unions in TypeScript, and how do they enable exhaustiveness checking?
- •Assigning the narrowed value to a variable typed `never` in the default/else branch causes a compile error if a new union member is ever added without being handled — this is 'exhaustiveness checking'.
- •This pattern models real-world state machines cleanly, e.g. a `{ status: 'loading' } | { status: 'success', data: T } | { status: 'error', error: string }` request state.
Q24: What is the difference between the `unknown` and `any` types in TypeScript?
- •`unknown` is the type-safe counterpart to `any` and is the recommended type for values from untrusted sources like JSON.parse or catch-block error variables.
- •Because `unknown` forces narrowing before use, it catches at compile time exactly the class of bugs `any` would silently let through to runtime.
Q25: How is `this` determined in a regular JavaScript function, and how do arrow functions differ?
- •The four rules apply in priority order: `new Fn()` (new binding) beats `fn.call(obj)`/`fn.apply(obj)`/`fn.bind(obj)` (explicit) beats `obj.fn()` (implicit) beats a bare `fn()` call (default: undefined in strict mode, global object otherwise).
- •Because arrow functions ignore all four rules and just capture the surrounding `this`, they are the standard fix for callbacks that need access to a class instance's `this` (e.g. class field arrow methods, or arrow callbacks inside methods).
Q26: Why does `this` become undefined when you extract a method and pass it as a bare callback, e.g. setTimeout(obj.method, 1000)?
- •Fixes include binding explicitly (`obj.method.bind(obj)`), wrapping in an arrow function (`() => obj.method()`), or defining the method as a class field arrow function so it's already bound per-instance.
- •This same detachment bug is why destructuring a method off an object (`const { method } = obj`) and calling it later also loses `this`.
Q27: What is the difference between == and === in JavaScript, and what does the abstract equality algorithm actually do?
- •Notable == special cases worth memorizing: null == undefined is true (and only equal to each other), while both are unequal to everything else including 0, false, or ''.
- •Comparing a number to a string coerces the string to a number; comparing a boolean to anything coerces the boolean to a number first — these chained conversions are what produce results like '' == 0 being true.
Q28: Explain the results of these expressions and why: [] + [], [] + {}, and '5' + 3 versus '5' - 3.
- •The + operator first calls ToPrimitive on both operands; if either primitive result is a string, it does string concatenation, otherwise numeric addition.
- •Every other arithmetic operator (-, *, /) has no string-concatenation meaning, so they always coerce both operands to numbers via ToNumber, which is why '5' - 3 cleanly gives 2.
Mistakes That Sink Otherwise Strong Candidates
Why it happens: Developers coming from other languages assume each iteration gets its own variable, not realizing `var` is function-scoped and shared across all iterations.
The fix: Use `let` for loop counters so each iteration gets a fresh binding, or explicitly capture the value in an IIFE/extra function argument.
Why it happens: `this` looks like a normal variable, so it's easy to forget it is re-derived from the call site every single time the function is invoked, not fixed at definition time.
The fix: Bind explicitly with .bind(this), use an arrow function wrapper, or define the method as a class field arrow function.
Why it happens: Primitives compare by value, so developers extend that intuition to objects, not realizing objects compare by reference.
The fix: Use a dedicated deep-equality check (structuredClone + compare, or a library like lodash's isEqual) when value equality is actually required.
Why it happens: It's the fastest way to make a red squiggly line disappear under deadline pressure, and the error doesn't resurface until runtime.
The fix: Reach for `unknown` plus a type guard, or a proper generic, so the compiler still catches misuse instead of silently allowing anything.
Why it happens: Code paths that 'normally' succeed in development make it easy to forget the error branch, especially inside fire-and-forget async calls.
The fix: Wrap awaited calls in try/catch, attach .catch() to any Promise chain that isn't awaited, and add a global unhandledrejection listener as a safety net.
Why it happens: Writing `for (const x of items) { await doThing(x); }` reads naturally and passes tests with small datasets, hiding the linear-latency cost until scale.
The fix: Start all the Promises first (e.g. items.map(doThing)) and await them together with Promise.all when the operations don't depend on each other.
Why it happens: It looks convenient because the method instantly becomes available everywhere, without realizing every object/array in the program — including third-party code — is affected.
The fix: Write a standalone utility function instead of extending a built-in prototype, and never let untrusted merge/clone input reach __proto__ or constructor keys.
Why it happens: == works intuitively for the common cases (comparing a string and number that clearly represent the same value), masking the much stranger edge cases until they appear in production data.
The fix: Default to === everywhere, and only reach for == deliberately (e.g. `x == null` to check for both null and undefined in one expression).
Quick-Reference Cheat Sheet
Recommended Practice Quizzes on QuizCluster
Test your retention and prepare for timed live coding and MCQ technical screening rounds:
JavaScript & TypeScript
Drill closures, prototypes, async patterns, and TypeScript's generics and utility types across hundreds of scenario-based questions.
JavaScript Engine & React Internals
Go deeper into the event loop, microtask ordering, garbage collection, and how the engine executes async JavaScript under the hood.
Frequently Asked Questions
Do I need to memorize TypeScript's advanced conditional types to pass a mid-level interview?
No. Mid-level rounds mostly test generics, the common utility types (Partial, Pick, Omit, Record), and interface vs type. Conditional types with infer show up more often in senior/staff rounds or when a role emphasizes library/SDK design.
Is it worth practicing the exact console.log ordering questions for the event loop?
Yes — they are extremely common because they cheaply verify you understand microtask-vs-macrotask priority, which underlies almost every real async bug (race conditions, stale closures in loops, UI jank from blocking code).
Should I learn class-based or closure-based patterns first for JavaScript interviews?
Learn closures first. Classes are sugar over prototypes and closures are the more fundamental mechanism; understanding closures makes both prototypal inheritance and the module pattern much easier to reason about.
How much TypeScript do frontend interviews actually require versus plain JavaScript?
Most frontend roles in 2026 expect working fluency in TypeScript (generics, utility types, basic narrowing) since the majority of production codebases have migrated to it, but the underlying JavaScript runtime questions (event loop, closures, `this`) are still asked just as often, if not more.