Closure Memory Leaks — Top JS Interview Questions
Heap usage grows monotonically — closures in Express route handlers retaining req objects cause OOM.
20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Closures bundle functions with their lexical scope, enabling data privacy.
- The Event Loop runs microtasks before macrotasks every cycle.
- Prototypal inheritance links objects;
thisdepends on call site. - Hoisting moves declarations; let/const live in the Temporal Dead Zone.
- Promises are microtasks; async/await is syntactic sugar over promises.
- Performance: heavy closure usage can retain memory longer than expected.
Closure memory leaks occur when a function retains references to variables from its outer scope, preventing those variables from being garbage collected even after the outer function has finished executing. This happens because JavaScript's garbage collector uses reference counting — any variable still reachable through a closure's scope chain stays alive.
In single-page applications with long-lived components (like React hooks or event listeners in a SPA), a single forgotten closure can pin entire object graphs in memory, causing the app to bloat over time. Interviewers ask about this because it tests your understanding of both closure mechanics and the V8 garbage collector's behavior under real-world conditions.
Closures themselves are fundamental to JavaScript — they're functions that remember the environment in which they were created. Every function in JavaScript is technically a closure, but the practical concern is when closures outlive their expected lifespan.
For example, attaching an event handler inside a React useEffect without proper cleanup creates a closure that holds references to the component's state and props. If that handler is never removed, the component's entire closure scope remains in memory even after unmounting.
This is why modern frameworks emphasize cleanup functions and why tools like Chrome DevTools' Memory panel are essential for debugging.
The ecosystem offers alternatives depending on the use case. For simple callbacks, you can use weak references via WeakMap or WeakSet to avoid pinning objects. For event listeners, the AbortController API provides a cleaner way to manage lifecycle.
When closures are unavoidable (and they often are), patterns like nullifying references or using useCallback with stable dependencies help. The key insight is that closures aren't inherently bad — they're a powerful tool that requires discipline. Understanding when and why they leak separates senior engineers who can build memory-efficient applications from those who wonder why their app crashes after 30 minutes of use.
Think of a JavaScript interview like a driving test. The examiner doesn't just want to see you turn a steering wheel — they want to know you understand WHY you check mirrors before changing lanes, WHEN to brake, and WHAT happens if you don't. These 50 questions work exactly the same way: they're not trivia, they're probes to see if you truly understand the road, not just the pedals.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
JavaScript powers over 98% of websites on the internet, runs on servers via Node.js, and has quietly become one of the most in-demand skills in tech. Whether you're applying at a scrappy startup or a FAANG company, the JavaScript interview is a rite of passage — and it's notoriously tricky because the language itself has sharp edges that even experienced devs fall on.
The real problem with most interview prep is that it focuses on 'what' instead of 'how.' You might know that var is function-scoped, but do you understand how the Execution Context and the Scope Chain actually handle it during the creation phase? Understanding these internals is what separates a junior developer from a senior engineer who can debug complex race conditions in an event-driven architecture.
By the end of this article, you will master the 50 most critical questions, from the nuances of Prototypal Inheritance and Closures to the modern complexities of the Event Loop and Asynchronous patterns. We provide production-grade code for every answer, ensuring you aren't just memorizing definitions, but building functional intuition.
How Closures Leak Memory — And Why Interviewers Care
A closure is a function that retains access to its lexical scope even when executed outside that scope. The core mechanic: inner functions hold references to outer variables, preventing garbage collection. This is not a bug — it's how JavaScript preserves state in callbacks, event handlers, and module patterns. But every retained reference is a memory cost.
In practice, closures keep the entire scope chain alive. A single closure referencing a small variable in a large outer function forces the entire outer scope — including arrays, DOM nodes, or heavy objects — to stay in memory. This is O(1) per reference but O(n) in retained objects. The key property: you cannot see the leak; it silently grows heap usage until the tab crashes or the page janks.
Use closures intentionally for encapsulation and stateful callbacks. But in production systems — especially long-lived SPAs or Node.js servers — every closure you attach to a DOM element or a global event listener is a potential leak. The rule: if you bind a closure, you must unbind it. Otherwise, the entire scope graph stays rooted, and the GC cannot reclaim it.
Understanding the Foundation: Closures and Scope
One of the most frequent 'Senior' level questions is: 'Explain closures and how they impact memory.' A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). In simpler terms, a closure gives a function access to its outer scope even after the outer function has finished executing. This is foundational for data privacy and factory patterns in JavaScript.
But here's the production trap: each time you create a closure, you keep the entire lexical scope alive. If that scope contains a large array or DOM reference, memory won't be released until the closure itself is garbage collected. That's why you'll see memory leaks in Node.js servers that create closures inside routes without careful cleanup.
/** * io.thecodeforge: Encapsulation using Closures * Demonstrates private state that cannot be accessed directly. */ function createForgeAccount(initialBalance) { let balance = initialBalance; // Private variable return { deposit: function(amount) { balance += amount; console.log(`New balance: ${balance}`); }, getBalance: function() { return balance; } }; } const account = createForgeAccount(1000); account.deposit(500); // New balance: 1500 console.log(account.balance); // undefined - state is protected
req object.The Event Loop: Asynchronous JavaScript Internals
Interviewers love to test your understanding of the Event Loop. They often ask: 'What is the difference between a Task (Macrotask) and a Microtask?' In the JavaScript runtime, Microtasks (like Promise.then and process.nextTick) always execute before the next Macrotask (like setTimeout or setInterval) in the loop. Understanding this priority is essential for predicting execution order in complex applications.
This order directly affects your UI rendering. A long-running microtask queue can freeze the page because rendering is a macrotask that waits for all microtasks to clear. That's why heavy synchronous work inside Promise.then blocks can degrade perceived performance.
/** * io.thecodeforge: Visualizing Task vs Microtask Priority */ console.log('1. Script start'); setTimeout(() => { console.log('5. SetTimeout (Macrotask)'); }, 0); Promise.resolve().then(() => { console.log('3. Promise 1 (Microtask)'); }).then(() => { console.log('4. Promise 2 (Microtask)'); }); console.log('2. Script end');
.then() can block the UI for seconds.Prototypal Inheritance and the `this` Keyword
Every object in JavaScript has an internal [[Prototype]] link that points to another object. When you access a property that doesn't exist on the object, JavaScript walks the prototype chain until it finds the property or reaches null. This is prototypal inheritance — a dynamic, delegation-based model quite different from classical inheritance.
The this keyword is determined entirely by the call site, not where the function is defined. That's why methods lose their context when passed as callbacks. The fix: arrow functions (lexical this), .bind(), or storing a reference to this outside.
/** * io.thecodeforge: Prototypal inheritance and dynamic this */ const vehicle = { type: 'generic', getType() { return this.type; } }; const car = Object.create(vehicle); car.type = 'car'; console.log(car.getType()); // 'car' – prototype chain works const lost = car.getType; console.log(lost()); // undefined – `this` is global (or undefined in strict) const bound = car.getType.bind(car); console.log(bound()); // 'car'
this defaults to the global object (window) in non‑strict mode, or undefined in strict mode. Always check the call site.this in React class components was the #1 cause of cannot-read-property errors in 2015 codebases.this is undefined, your function is being called without context.this is bound by call site, not definition.this.this.Hoisting and the Temporal Dead Zone
JavaScript hoists variable and function declarations to the top of their scope. But var is initialized with undefined, while let and const are hoisted to a 'temporal dead zone' — they exist but cannot be accessed until the declaration line is reached. Accessing them before that throws a ReferenceError.
This is a frequent source of bugs when code relies on order of declarations. Senior devs treat let and const as block‑scoped and never assume they're initialized before the declaration.
/** * io.thecodeforge: Hoisting and Temporal Dead Zone */ console.log(a); // undefined (var hoisted, initialised) var a = 5; console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 10; function demo() { console.log(c); // ReferenceError let c = 1; }
var as an elevator already on the ground floor (undefined). let/const is an elevator still in the shaft—you can't step in until it arrives at your floor.- All declarations are hoisted to the top of their scope.
vargets initialized withundefinedimmediately.letandconstare hoisted but not initialized—they live in the TDZ.- TDZ ends when the actual declaration line executes.
- Accessing a variable in its TDZ throws a ReferenceError.
let inside a switch/case block without braces.case clauses share the same block scope, causing redeclaration errors.case in curly braces to create a new block scope.let and const are hoisted but not initialized.const by default, let when reassignment needed.Promises, Async/Await and Error Handling
Promises represent the eventual completion (or failure) of an asynchronous operation. They replaced callbacks and enable chaining with .then(). async/await is syntactic sugar that makes promise chains read like synchronous code. But under the hood, await waits for a promise to settle — it doesn't block the event loop.
A critical detail: unhandled promise rejections still crash Node.js (since v15). Always append a .catch() or use a try/catch around await. Missing this causes silent data loss in production when an API call fails.
/** * io.thecodeforge: Proper async error handling */ async function fetchUserData(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error('Request failed'); return await response.json(); } catch (error) { // Log structured error, don't just rethrow console.error({ message: error.message, userId, timestamp: Date.now() }); throw error; // Propagate if caller needs to know } }
.then() handler to maintain the chain. A missing return turns the next handler into a dangling promise.process.on('unhandledRejection', handler) globally, but better: catch every promise.async/await is a wrapper around promises.await deserves a try/catch.Why `===` Still Bites You in 2024 (And Your Interviewer Knows It)
You think you understand strict equality? Good. Now explain why +0 === -0 returns true but Object.is(+0, -0) returns false. That’s not a trivia trick — that’s a production bug that corrupted a trading dashboard I once debugged at 2 AM.
JavaScript’s === uses the SameValueZero algorithm for NaN and +0/-0. It makes NaN === NaN false (correct per IEEE 754), but it deliberately treats signed zeroes as equal. Most code never hits this. But when you’re hashing values, caching results, or doing math with signed zero (atan2, for example), === silently loses the sign.
Interviewers ask this because it separates people who memorised the docs from people who lived through the carnage. The fix? Use Object.is() when you need true bit-level equality. Use === when you want signed zeroes collapsed. Know the difference. Your interviewer is watching for the flinch when you say "strict equality is simple."
// io.thecodeforge — interview tutorial // Simulating JavaScript's broken cache key in Python # A dashboard aggregator that cached price deltas # Signed zeroes from atan2 corrupted the cache import math price_deltas: dict[tuple[float, float], str] = {} def cache_delta(source: float, angle: float) -> str: # 'hash((+0.0, 0.0))' == 'hash((-0.0, 0.0))' just like JS === key = (source, math.atan2(source, angle)) if key not in price_deltas: price_deltas[key] = f"computed" return price_deltas[key] # Two different angles, same signed zero issue r1 = cache_delta(0.0, 1.0) # atan2(0, 1) -> 0.0 r2 = cache_delta(-0.0, 1.0) # atan2(-0, 1) -> -0.0 # Both should be separate entries but they collide print(f"Keys: {list(price_deltas.keys())}") print(f"Cache size: {len(price_deltas)}") # Should be 2, prints 1
===, you'll silently coalesce signed zeroes. Use Object.is() for hashing, or better, avoid floats as keys altogether.=== uses SameValueZero (treats +0 and -0 as equal). Use Object.is() when sign matters.Your Debounce Is Lying to You (And So Is Every Interview Question)
Every junior can parrot "debounce waits for a pause, throttle limits rate." Cute. In production, debouncing a save button causes data loss when the user mashes Enter. I’ve seen it kill a week of patient records because the last keystroke never settled.
Interviewers now ask: "Design a leading-edge throttle with a trailing guarantee." They want to see if you understand the _why_ — not just copy-paste from a blog. A leading-edge throttle fires immediately on the first call, then blocks subsequent calls for the window. A trailing guarantee ensures the last suppressed call fires after the window expires. Without it, you get liveness failure (stale UI) or correctness failure (lost data).
Here’s the pattern: flag-based lock, timer resets the lock, a pending callback captures the final arguments. Don’t use _.throttle defaults — they lag. Don’t use _.debounce(maxWait) unless you’ve tested the edge-case where no trailing call fires.
Interviewers watch for the moment you realise your setTimeout(fn, 0) inside a debounce is just a race condition with a timer. That’s the senior reflex.
// io.thecodeforge — interview tutorial # JavaScript-like leading throttle with trailing call import time from typing import Callable, Optional class LeadingThrottle: def __init__(self, fn: Callable, window_ms: int): self.fn = fn self.window = window_ms / 1000 self.locked = False self.pending: Optional[list] = None def __call__(self, *args): if not self.locked: self.fn(*args) # leading execution self.locked = True self._start_timer() else: self.pending = args # stash last call def _start_timer(self): # block subsequent calls, then fire trailing def release(): self.locked = False if self.pending is not None: last_args = self.pending self.pending = None self.fn(*last_args) # trailing execution # Simulate setTimeout with a real timer import threading threading.Timer(self.window, release).start() # Usage simulation log = [] def save(data: str): log.append(data) throttled_save = LeadingThrottle(save, window_ms=200) throttled_save("A") throttled_save("B") throttled_save("C") time.sleep(0.3) print(f"Saved: {log}") # Leading 'A', trailing 'C'
parseInt's Hidden Radix Ambiguity: The Interviewer's Favourite Footgun
Write parseInt('0x1f') and you get 31. Write parseInt('0o7') and you get 0. Most devs shrug and move on. But your interviewer just saw you fail to explain that parseInt only auto-detects hex (0x) and octal via 0 prefix — but only for legacy 0-prefixed octal, not the 0o ES6 syntax. That inconsistency has caused millions in bug bounties.
The rule: parseInt first strips the quotes, then checks the first two characters. If it sees 0x or 0X, it uses radix 16. If it sees 0 followed by a digit 0-7, it uses radix 8 (in older engines), but modern engines treat 0 as decimal unless 0o is given — which parseInt does NOT recognise. It sees 0, stops at the first non-digit (o), and returns 0. Full stop.
Interviewers ask this because it reveals whether you understand parsing order, not just memorise MDN. The fix: always pass radix. Always. Yes, even for hex. parseInt('0x1f', 16) is explicit and immune to engine quirks. Never rely on auto-detection in code that touches user input, config, or network data.
// io.thecodeforge — interview tutorial # Demonstrating the ambiguity (Python has int() with base) # This mimics JavaScript's parseInt behavior def parse_int_js(value: str) -> int: """Simulates JavaScript's parseInt with radix detection.""" s = value.strip() if s.startswith(('0x', '0X')): # hex auto-detected return int(s, 16) if s.startswith('0o'): # ES6 octal - NOT auto-detected by JS parseInt # Falls back to decimal, returns 0 because 'o' stops parsing if len(s) > 2 and s[2].isdigit(): return int(s[2:], 8) # what JS SHOULD do return 0 if len(s) > 1 and s[0] == '0' and s[1] in '01234567': # legacy octal (0-prefixed) return int(s, 8) return int(s, 10) print(parse_int_js('0x1f')) # 31 print(parse_int_js('0o7')) # 0 (simulates JS failure) print(parse_int_js('077')) # 63 (legacy octal) # The safe way: always specify radix def safe_parse_int(value: str, radix: int = 10) -> int: return int(value, radix) print(safe_parse_int('0x1f', 16)) # 31
radix in ESLint (enforce). Never rely on parseInt auto-detection for hex or any other base. Explicit radix prevents silent 0s in production.parseInt only auto-detects hex (0x). ES6 octal (0o) returns 0. Always pass the radix argument — no exceptions.Memoization: The Caching Trick That Makes You Look Like a Senior
Memoization is not a buzzword. It's a performance optimization pattern that caches function results based on input arguments. When you call a pure function with the same arguments twice, memoization returns the cached result instead of recomputing.
Why do interviewers love this? Because it tests your understanding of closures, pure functions, and space-time tradeoffs. A Fibonacci calculator that explodes into O(2^n) recursion becomes O(n) with memoization. That's the difference between a junior writing nested loops and a senior who knows when to cache.
Real production use cases: expensive API call normalization, repeated DOM calculations, or any idempotent operation. But beware — memoization is a trap for impure functions or functions with side effects. If you cache a function that reads from a rapidly changing data source, you'll serve stale data. That's how prod incidents happen.
// io.thecodeforge — interview tutorial def memoize(func): cache = {} def wrapper(*args): if args in cache: print(f"Cache hit: {args}") return cache[args] result = func(*args) cache[args] = result return result return wrapper @memoize def slow_fetch(user_id: int) -> str: print(f"Computing for {user_id}...") return f"data_{user_id}" print(slow_fetch(42)) print(slow_fetch(42))
lru_cache in Python or Map with WeakRef in JavaScript for automatic cleanup.Recursion: Elegant, Until Your Stack Blows Up
Recursion is when a function calls itself to solve a smaller version of the same problem. It's elegant for tree traversal, factorial calculation, and parsing nested structures. But recursion has a hard ceiling: the call stack. Every call pushes a frame onto the stack. Too many frames? Stack overflow. Your app crashes.
Senior engineers know that recursion is not always the answer. Interviewers ask about recursion to see if you understand tail call optimization, stack depth limits (typically ~10k frames), and when to convert to an iterative loop. A recursive Fibonacci without memoization is O(2^n) and will overflow at n=40. An iterative loop? O(n) and never overflows.
Production rule of thumb: If recursion depth exceeds 1000, rewrite iteratively. For DOM traversal or file systems, recursion is natural. For flat list operations, it's overengineering. Know your base case. Know your stack limit. Or your code will silently die in production.
// io.thecodeforge — interview tutorial def factorial(n: int) -> int: if n <= 1: return 1 return n * factorial(n - 1) print(factorial(5)) # Dangerous: no tail recursion in Python try: factorial(1000) except RecursionError as e: print(f"Stack blew up: {e}")
Rest and Spread: The Operators That Tame Function Arguments
The rest parameter (...args) collects remaining function arguments into an array. The spread operator (...iterable) expands an array into individual elements. They look identical, but context determines behavior. Rest is for gathering: function sum(...nums). Spread is for scattering: Math.max(...array).
Interviewers hammer this because it tests your grasp of immutability and function signatures. Spread creates shallow copies — a junior's biggest mistake is trying to deep-clone nested objects with {...obj} and wondering why the original mutated. Rest eliminates the need for the arguments object (which is not a real array). Use rest; your code is cleaner, and you avoid prototype chain bugs.
Production reality: You use these every day. Copying state in React? That's spread. Destructuring configuration objects with default values? That's rest. Building variadic functions like createLogger(level, ...tags)? Rest. But remember: spread is O(n). Spreading a 10k-element array in a hot loop will make your CPU cry. Use it deliberately.
// io.thecodeforge — interview tutorial def log(level: str, *messages: str) -> None: # *messages is rest: collects remaining args into a tuple for msg in messages: print(f"[{level}] {msg}") def merge(*dicts: dict) -> dict: # Spread-like behavior via dict unpacking result = {} for d in dicts: result = {**result, **d} # spread return result log("INFO", "Started", "Connecting", "Done") print(merge({"a": 1}, {"b": 2}))
structuredClone() or JSON.parse(JSON.stringify(obj)) for deep clones in production.Top Mistakes That Sink JavaScript Interview Performances
Interviewers watch for subtle mistakes that signal shallow understanding. The most damaging? Mutating function arguments directly—this side-effects callers and breaks predictability. Another classic: assuming Array.sort() sorts numerically without a comparator—it defaults to string conversion, so [10, 2].sort() gives [10, 2]. Async errors also trip candidates: forgetting to await inside a forEach loop causes silent failures because forEach does not handle promises. Finally, misreading null vs undefined in equality checks leads to runtime bugs. These mistakes reveal missing fundamentals, not just typos. Avoid them by understanding why JavaScript behaves this way: type coercion, reference vs value semantics, and synchronous iteration of async operations. Practicing intentional debugging of these patterns during study time builds the muscle memory interviewers expect.
// io.thecodeforge — interview tutorial // BUG: mutates input array function addScore(scores, val) { scores.push(val); // side effect on caller return scores; } // FIX: return a new array function addScoreImmutable(scores, val) { return [...scores, val]; } // BUG: numeric sort fails let nums = [10, 2, 1]; nums.sort(); // [1, 10, 2] — string order // FIX: comparator nums.sort((a, b) => a - b); // [1, 2, 10] // BUG: unhandled promise in forEach async function process(items) { items.forEach(async (item) => { await fetch(item); // fire-and-forget }); }
Study Strategy That Matches Interviewer Expectations
Interviewers don't want memorized answers—they want problem-solving rhythm. Start each problem by restating it aloud, then ask two clarifying questions: 'What happens with empty input?' and 'Should I mutate the argument, or return a new value?' This pattern alone separates strong candidates from average ones. Next, always sketch a brute-force solution first, then optimize once. Verbalizing your trade-offs (time vs space, readability vs performance) shows systems thinking. For hands-on prep, practice coding on a whiteboard or plain editor—no IDE autocomplete. Run your code mentally, step by step, tracking state. Finally, rehearse explaining why your code works, not just what it does. Interviewers drill into edge cases like undefined, null, and NaN to test your depth. Study those specifically: coercion rules, falsy values, and floating point quirks.
// io.thecodeforge — interview tutorial // Step 1: clarify inputs/outputs function maxProfit(prices) { // Q: empty array? return 0? // Q: can we mutate prices? // Step 2: brute force (O(n^2)) let max = 0; for (let i = 0; i < prices.length; i++) { for (let j = i + 1; j < prices.length; j++) { max = Math.max(max, prices[j] - prices[i]); } } return max; } // Step 3: optimize (O(n)) function maxProfitOpt(prices) { let minPrice = Infinity; let maxProfit = 0; for (let price of prices) { minPrice = Math.min(minPrice, price); maxProfit = Math.max(maxProfit, price - minPrice); } return maxProfit; }
7. Is JavaScript a Statically Typed or a Dynamically Typed Language?
JavaScript is a dynamically typed language. This means that variable types are determined at runtime, and you can reassign a variable to a value of a different type without any compile-time enforcement. For example, let x = 42; x = "hello"; is perfectly valid. This flexibility speeds up prototyping but introduces bugs that static typing catches early—null is an object, empty arrays are truthy, and addition can concatenate when you expect arithmetic. Modern tools like TypeScript add static type checking before execution, but vanilla JavaScript remains dynamically typed. In interviews, expect follow-up questions about type coercion quirks (e.g., [] + {} vs {} + []) and the typeof operator's infamous edge cases (typeof null === 'object'). Master the runtime behavior: variables hold values, not types, and typeof is the only way to check primitives at runtime.
// io.thecodeforge — interview tutorial // <25 lines function checkType(val) { console.log(typeof val); } checkType(42); // 'number' checkType('hello'); // 'string' checkType(null); // 'object' — infamous bug checkType([]); // 'object' checkType(undefined); // 'undefined' let x = true; x = 'now string'; // dynamic reassignment console.log(typeof x); // 'string'
Array.isArray() and val === null for robust checks.JavaScript Essentials Often Missed in Interviews
Four critical topics slip through the cracks: localStorage, sessionStorage, cookies, and basic algorithms like prime checking. localStorage stores up to 5-10MB of data per domain with no expiration—persists even after closing the browser. sessionStorage matches that capacity but clears when the tab closes. Cookies are older, smaller (4KB), sent with every HTTP request, and customizable with expiration and path flags—essential for server-side auth tokens. For the programming challenge: a prime check must handle edge cases (numbers ≤ 1 are not prime) and optimize by checking only up to sqrt(n) after early evens. Interviewers watch for efficiency reasoning—you'll stand out by explaining why naive loops fail on large inputs. Also prepare for 'what if input is not a number'—defensive coding wins points.
// io.thecodeforge — interview tutorial // <25 lines function isPrime(num) { if (typeof num !== 'number' || !Number.isInteger(num)) { return false; } if (num <= 1) return false; if (num === 2) return true; if (num % 2 === 0) return false; const sqrt = Math.sqrt(num); for (let i = 3; i <= sqrt; i += 2) { if (num % i === 0) return false; } return true; } console.log(isPrime(17)); // true console.log(isPrime(1)); // false
Closure Memory Leak in a Node.js Microservice
req object, which after the response was sent, still held a reference to a large parsed payload via an inner function used for logging. The closure never released it.- Closures retain their lexical scope as long as any reference exists.
- Always profile memory after adding closures in long-lived processes.
- Limit closure scope to only the variables you need — avoid capturing large objects.
setTimeout(fn, 0) runs after a Promise.then() even though both appear in sequence.console.log markers. Microtasks (promises) drain before the next macrotask (setTimeout).await the promise inside the async function. Without await, the function returns a pending promise.for loop with setTimeout inside logs the same final value multiple times.let (block scope) or an IIFE to capture each iteration's value.console.log(i) inside setTimeout to see what prints.Check if the loop uses `var` or `let`.let i = 0; i < n; i++ or wrap setTimeout in an IIFE: (function(j){ setTimeout(() => console.log(j), 0); })(i).Add a `.catch()` to see if there's an unhandled rejection.Log the promise object — does it remain pending?.then() returns a value or promise. Missing return breaks the chain.Log `this` inside the callback to confirm it's undefined or window.Check if the callback is passed as a reference without binding.handleClick = () => { ... } (class field proposal) or bind in constructor.| Feature | Var | Let / Const |
|---|---|---|
| Scope | Function Scoped | Block Scoped ({}) |
| Hoisting | Hoisted (initialized as undefined) | Hoisted (uninitialized - TDZ) |
| Re-declaration | Allowed | Not Allowed |
| Global Object | Creates property on 'window' | Does not create 'window' property |
| Temporal Dead Zone | No | Yes (ReferenceError before declaration) |
| File | Command / Code | Purpose |
|---|---|---|
| io | /** | Understanding the Foundation |
| io | /** | The Event Loop |
| io | /** | Prototypal Inheritance and the `this` Keyword |
| io | /** | Hoisting and the Temporal Dead Zone |
| io | /** | Promises, Async/Await and Error Handling |
| SignedZeroCollision.py | price_deltas: dict[tuple[float, float], str] = {} | Why `===` Still Bites You in 2024 (And Your Interviewer Know |
| LeadingEdgeThrottle.py | from typing import Callable, Optional | Your Debounce Is Lying to You (And So Is Every Interview Que |
| ParseIntTrap.py | def parse_int_js(value: str) -> int: | parseInt's Hidden Radix Ambiguity |
| memoize.py | def memoize(func): | Memoization |
| recursion.py | def factorial(n: int) -> int: | Recursion |
| rest_spread.py | def log(level: str, *messages: str) -> None: | Rest and Spread |
| Mistakes.py | function addScore(scores, val) { | Top Mistakes That Sink JavaScript Interview Performances |
| PrepStrategy.py | function maxProfit(prices) { | Study Strategy That Matches Interviewer Expectations |
| dynamic_type_check.py | function checkType(val) { | 7. Is JavaScript a Statically Typed or a Dynamically Typed L |
| is_prime_check.py | function isPrime(num) { | JavaScript Essentials Often Missed in Interviews |
Key takeaways
this is bound by call site; use arrow functions or .bind() to preserve context.let/const have a Temporal Dead Zoneawait needs a try/catch or the promise must have a .catch() to avoid unhandled rejections.Common mistakes to avoid
5 patternsMemorising syntax before understanding 'Hoisting' and 'Temporal Dead Zone'
let throws ReferenceError before initialization; misuses var thinking it's block-scoped.console.log before and after declarations.Skipping practice with `this` keyword binding (call, apply, bind)
this is undefined in event handlers.console.log(this) inside the function to confirm.Ignoring the difference between `==` and `===`
0 == false returns true.=== (strict equality) by default. Enable ESLint rule eqeqeq to enforce it.Not returning a promise from an `async` function
undefined instead of the expected resolved value.async functions always have a return statement when they need to provide a value. The returned value is automatically wrapped in a resolved promise.Assuming `setTimeout(fn, 0)` runs immediately
setTimeout to defer execution still runs after all synchronous code and microtasks, causing incorrect order.setTimeout adds a macrotask. Use Promise.resolve().then() for 'as soon as possible' after current task.Interview Questions on This Topic
Explain closures and give a real-world use case.
useCallback which relies on closure to memoize functions. Example: a counter that increments privately without exposing the variable to global scope.What is the difference between `==` and `===`?
== performs type coercion before comparison, e.g., '5' == 5 is true. === requires both value and type to be equal, no coercion. Always use === to avoid subtle bugs. The only exception is when explicitly checking for null or undefined with == (since null == undefined is true).How does the Event Loop work in JavaScript?
Promise.then() runs before setTimeout(fn, 0).What is the Temporal Dead Zone?
let/const are hoisted) and the actual declaration line. Variables are uninitialized during this period. Accessing them throws a ReferenceError. Contrast with var which is initialized to undefined upon hoisting. The TDZ ensures you cannot use a variable before its intended initialization, preventing common bugs.Explain `async/await`? How does it relate to Promises?
async/await is syntactic sugar over Promises. An async function always returns a Promise. Inside it, await pauses execution until the awaited Promise settles, then resumes with the result. This makes asynchronous code read like synchronous code. Under the hood, the runtime uses generators or a state machine. Error handling is done with try/catch blocks instead of .catch() chains.Frequently Asked Questions
The TDZ is the period between the start of a block's execution and the moment a variable declared with 'let' or 'const' is initialized. If you try to access the variable during this period, JavaScript will throw a ReferenceError, unlike 'var' which would return undefined.
In JavaScript, every object has a hidden property called [[Prototype]] that links to another object. When you try to access a property that doesn't exist on an object, JS looks up the 'prototype chain' until it finds it or reaches null. This allows objects to share methods without duplicating them in memory.
'undefined' means a variable has been declared but has not yet been assigned a value. 'null' is an assignment value that represents the intentional absence of any object value. Effectively, undefined is the default state, while null is a deliberate state set by the developer.
creates a new function that, when called, has its bind()this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called. It does not invoke the function immediately. Example: const boundLog = console.log.bind(console, 'Prefix').
Both invoke a function with a given this value. call takes arguments as a comma-separated list, while apply takes them as an array. Example: fn.call(obj, a, b) vs fn.apply(obj, [a, b]).
20+ years shipping production code across the stack, with years spent interviewing engineers. Notes here come from systems that actually shipped.
That's JavaScript Interview. Mark it forged?
9 min read · try the examples if you haven't