Nice set of deep JavaScript questions — I’ll answer each one thoroughly with code examples, edge-cases and practical tips you can apply right away.
1) Implement your own Promise.allSettled() and handle edge cases
Promise.allSettled(iterable) returns a promise that resolves after all of the input promises settle (either fulfilled or rejected). The result is an array of objects { status: "fulfilled", value } or { status: "rejected", reason } in the same iteration order.
Here's a robust polyfill that handles common edge cases:
-
Accepts non-iterable → throws
TypeError. -
Accepts non-Promise values → treats them as fulfilled.
-
Preserves order of input.
-
Handles thenables (objects with
.then) safely (wrap withPromise.resolve). -
Works with sparse arrays and other iterables.
Usage & examples
Edge cases to watch
-
If the input is not iterable (e.g.,
null), throwTypeError(matches built-in). -
If a thenable’s
.thenthrows synchronously,Promise.resolvewill turn it into a rejection — our wrapper handles it. -
If you want the native behavior in older engines, ensure
finallyexists onPromise.prototypeor polyfillfinally.
2) Optimizing expensive computations using memoization
Approach / principles
-
Memoization caches results of pure functions (same inputs → same output) to avoid recomputation.
-
Key decisions:
-
How to represent keys: primitive arguments are easy; objects require
WeakMapor serialization. -
Cache eviction: unlimited cache may leak memory — use LRU or size limit.
-
Synchronous vs asynchronous functions: memoizing Promises needs care (cache the Promise itself).
-
Side-effect free: ensure the function is pure, or memoization will cause stale results.
-
Simple sync memoize (primitives)
Better: Support object args with WeakMap
-
Use a nested WeakMap/Map structure keyed by each argument to avoid serializing objects and to let garbage collection free memory when objects are gone.
Async memoize (cache in-flight Promises)
If fn returns a Promise, cache the Promise so concurrent callers share the same in-flight work. Also handle rejection — sometimes you want to remove cache entries on rejection.
LRU cache example (prevent unbounded memory)
Use an LRU strategy for expensive computations that could blow up memory:
Practical tips
-
Use
WeakMapfor caching results keyed off objects to avoid memory leaks. -
If function takes large objects, consider hashing specific properties instead of serializing the entire object.
-
Don’t memoize functions that have side effects.
-
For CPU-heavy tasks, consider memoizing partial results (dynamic programming).
-
For UI expensive renders, use techniques like virtualization,
requestAnimationFrame,useMemoin React (with careful dependency arrays).
3) How the event loop prioritizes microtasks vs macrotasks in complex async flows
Short summary
-
A macrotask (task) queue contains things like
setTimeout,setInterval,setImmediate(Node), I/O callbacks, UI rendering tasks. -
A microtask queue contains Promise callbacks (
.then/.catch/.finally),queueMicrotask, andprocess.nextTick(special high-priority queue in Node). -
After the current macrotask finishes, the runtime runs all microtasks (until empty) before picking the next macrotask. That means microtasks always run earlier than the next macrotask.
Browser ordering (typical)
-
Start macrotask A (e.g., script execution or setTimeout callback).
-
During it, microtasks are queued (Promise callbacks).
-
When macrotask A completes, run the microtask queue until empty (new microtasks appended are also run).
-
Handle rendering/paint if needed.
-
Pick next macrotask B from task queue.
Node specifics
-
process.nextTickruns before other microtasks — Node treats it as higher priority than Promise microtasks. -
setImmediateandsetTimeouthave different phases in Node’s event loop; ordering can be subtle.
Example to illustrate ordering
Complex cases
-
If a microtask enqueues another microtask, it will run in the same microtask checkpoint (no macrotask in between).
-
If a microtask schedules a macrotask, the macrotask will run only after microtasks are drained and after the next macrotask selection.
-
requestAnimationFrameruns during rendering phase - happens between microtasks and next macrotask with paint? Practical behavior varies across browsers.
Node note
Why it matters
-
Microtasks allow prioritized work completion (e.g., promise chain resolution) before the UI or timers proceed.
-
If you flood microtasks (e.g., in a loop) you can starve rendering and macrotasks — avoid infinite microtask generation.
4) Build a custom localStorage wrapper with expiry & fallback logic
Requirements:
-
Save keys with TTL (expiry).
-
Safe JSON serialize/deserialize.
-
Fallback to in-memory store if
localStorageis unavailable (private mode, quotas). -
Gracefully handle storage corruption / quota exceeded.
-
Optional cleanup and cross-tab sync using
storageevent.
Here's a practical implementation:
Cross-tab sync
-
Use
window.addEventListener('storage', ...)to listen for changes from other tabs and update UI. -
The
storageevent is only fired in other windows (not the one that wrote).
Quota and corruption handling
-
Catch
QuotaExceededErrorwhensetItemfails; fallback to in-memory or try evicting expired items. -
On
JSON.parseerrors, treat as corrupted → remove and returnnull.
5) Explain prototype inheritance with a real example (not theory)
Let’s demonstrate with a concrete example: animals, and dogs inheriting from Animal. We'll use constructor functions and also show how prototypes are modified and how instances see changes.
Key "real" takeaways from the code
-
Animal.prototypestores methods shared by allAnimal(andDog) instances. Methods are not duplicated per instance. -
Instance properties defined in the constructor (
this.ancestry = [...]) are unique per instance. If you mistakenly put an array on the prototype, it will be shared — leading to bugs. -
Object.create(Animal.prototype)creates a fresh object whose[[Prototype]]isAnimal.prototype. This sets up inheritance without calling theAnimalconstructor again. -
Changing the prototype (adding methods) affects all instances immediately.
-
instanceofworks because of the prototype chain:d1 instanceof Dogandd1 instanceof Animalwill both be true.
Pitfall example: shared mutable prototype
Always put instance-specific state inside constructor, not on prototype.
6) Detecting & fixing memory leaks in a large JavaScript codebase
Common sources of leaks
-
Global variables — accidental globals (e.g.,
foo = 1) or long-lived objects onwindow. -
Forgotten timers and intervals —
setInterval,setTimeoutnot cleared. -
Event listeners not removed — on DOM nodes or global event emitters (Node
EventEmitter). -
Closures holding large objects — functions capturing large scopes and are kept alive.
-
Detached DOM nodes — DOM nodes removed from document but still referenced by JS (e.g., via closure/event listeners).
-
Unbounded caches — Maps/arrays/generic caches that grow without eviction.
-
Third-party library leaks — libraries creating hidden references.
Detection tools & techniques
-
Chrome DevTools: Memory tab
-
Heap snapshot (compare snapshots before/after action).
-
Allocation instrumentation / timeline: watch memory usage over time, see retained objects.
-
Record allocation stack traces to figure out where objects are created.
-
"Detached DOM tree" filter shows nodes not in document but kept alive.
-
-
Performance Timeline: watch for memory growth during interactions.
-
Node:
--inspectand Chrome DevTools for server processes;heapdump/v8-profilerfor heap snapshots. -
Process metrics:
process.memoryUsage()(Node) to monitor RSS/heap over time. -
Automated tests: simulate long flows and record memory.
Workflow to find a leak
-
Reproduce scenario that grows memory (e.g., opening/closing view repeatedly).
-
Take baseline heap snapshot.
-
Perform action repeatedly (e.g., open/close modal 100x).
-
Take another snapshot, compare retained objects and allocation stacks.
-
Identify objects that keep growing and their retaining paths.
-
Inspect code at the retainers to find listeners/timers/clojures/etc.
Examples & fixes
-
Forgotten event listeners
Leaky code:
Fix:
-
Interval left running
Leaky code:
Fix:
-
Detached DOM nodes
Symptoms: heap snapshot shows lots of nodes under Detached DOM tree.
Fix: ensure references to nodes are removed, listeners removed, and caches cleared. Use element.remove() and break closures that reference the element.
-
Unbounded caches
Leaky code:
Fix: use WeakMap for object keys, or LRU with size limit, or TTL eviction.
-
Large closure capturing big data
Leaky code:
Fix: move bigArray out or ensure you clear element.onclick = null when done; avoid capturing entire context if only small part needed.
Continuous monitoring
-
Add monitoring/alerts when memory grows beyond thresholds (Node: heap growth rate; Browser: use telemetry).
-
Run periodic memory tests in CI for critical pages/components.
Pro tips
-
Prefer
WeakMap/WeakSetfor caches keyed by objects so GC can release entries. -
Avoid creating functions inside render loops (component frameworks) without need — those create many closures.
-
For single page apps, carefully detach long-lived resources when changing routes.
-
Use
requestAnimationFramefor recurring UI work to avoid unnecessary timer scheduling.
Final practical checklist you can apply now
-
For async flows: remember microtasks (Promises) run before the next macrotask — that explains often-surprising ordering.
-
When memoizing: choose key strategy carefully (primitive JSON key vs WeakMap) and apply a bounded cache (LRU) in production.
-
For
allSettled: always wrap items withPromise.resolveand preserve iteration order. -
For client-side storage: always guard
localStorageaccess with try/catch and have an in-memory fallback. -
For leaks: instrument with heap snapshots, find retaining paths, then remove listeners/clear timers/dereference caches.
✅ JavaScript Execution Context – 15 Questions & Answers
1. What exactly is an Execution Context, and when is it created?
Answer:
An Execution Context is the environment where JavaScript code is evaluated and executed.
It is created whenever:
-
The JavaScript engine starts running your file (Global Execution Context)
-
A function is invoked (Function Execution Context)
-
You run code inside
eval()(Eval Execution Context)
Think of it like a “box” that contains everything needed to run the code inside it.
2. How many Execution Contexts can exist in memory at the same time?
Answer:
Many Execution Contexts can exist simultaneously in memory, but only one can run at a time because JavaScript is single-threaded.
Example: During nested function calls:
All contexts are stored in the Call Stack, but only the top one executes.
3. What are the three types of Execution Contexts in JavaScript?
Answer:
-
Global Execution Context (GEC)
Created when the JS file starts. -
Function Execution Context (FEC)
Created for each function call. -
Eval Execution Context
Created when code runs insideeval().
4. What happens internally during the Creation Phase of an Execution Context?
Answer:
The Creation Phase does 4 key things:
-
Create the Lexical Environment
-
Create the Variable Environment
-
Bind
thisvalue -
Hoisting happens:
-
var→ hoisted and initialized asundefined -
let/const→ hoisted but uninitialized (TDZ) -
Function declarations → hoisted with full function definition
-
5. What is the Call Stack, and how does it manage nested function calls?
Answer:
Call Stack is a LIFO (Last In First Out) stack that stores Execution Contexts.
Example:
Call Stack flow:
-
Push GEC
-
Push
a()FEC -
Push
b()FEC -
Pop
b() -
Pop
a() -
End
6. What exactly lives inside a Lexical Environment?
Answer:
A Lexical Environment contains:
1. Environment Record
-
Variables (
let,const,var) -
Function declarations
-
Parameters
-
Inner functions
2. Reference to Outer Lexical Environment
This enables scope chaining.
7. How does a Lexical Environment form the Scope Chain?
Answer:
Each Lexical Environment has a pointer to its parent environment.
Example:
Scope Chain inside b():
JavaScript searches variables up the chain.
8. What happens when JavaScript cannot find a variable in the current scope?
Answer:
It moves up the Scope Chain to the parent Lexical Environment.
If it reaches the Global Environment and still doesn't find it →
ReferenceError: variable is not defined
9. What is the difference between Scope and Execution Context?
| Feature | Scope | Execution Context |
|---|---|---|
| Definition | Rules that define where variables are accessible | Environment where code is executed |
| Created | At code write time | At runtime |
| Purpose | Controls visibility of variables | Stores variables, this, and manages execution |
| Examples | Block scope, function scope | Global EC, Function EC |
10. How is the value of this decided inside a Function Execution Context?
Answer:
this depends on HOW the function is called.
Rules:
-
Simple function call →
this = undefined(orwindowin non-strict mode) -
Method call →
this = the object -
Constructor →
this = new instance -
Arrow function → no own this (it inherits from parent scope)
-
call/apply/bind→ manually setthis
Example:
11. Why do let and const throw a ReferenceError before initialization?
Answer:
Because they are in the Temporal Dead Zone (TDZ).
They are hoisted, but not initialized, so accessing them before the initialization line results in:
12. What exactly is the Temporal Dead Zone, and when does it exist?
Answer:
TDZ is the period between:
-
variable hoisting
-
actual variable initialization
Example:
13. Why is Execution Context the foundation of Closures?
Answer:
Closures work because:
Even after a function has returned, its Lexical Environment is preserved in memory.
Example:
Even though outer() finished execution, its Execution Context stays alive due to closure.
14. What happens to a function’s Execution Context after it returns another function?
Answer:
Normally a function’s Execution Context is removed from the call stack.
But if an inner function uses its variables, that Lexical Environment is not garbage collected.
It is kept alive inside the closure.
So:
15. Why does understanding Execution Context directly improve debugging in React, Vue, and Angular?
Answer:
Because these frameworks heavily rely on:
-
closures (state, hooks)
-
scope behavior
-
asynchronous event loops
-
lexical scoping
-
call stack trace
Examples:
React
-
stale state closure issues
-
useEffect dependency issues
-
state updates batched in microtasks
Vue
-
reactive proxies depend on lexical closures
Angular
-
Zone.js patches event loop → closures store task contexts
Understanding Execution Context lets you debug:
✔ undefined variables
✔ closure bugs
✔ async behavior
✔ stale state
✔ memory leaks
REACT.JS — Actual Technical Round Q&A
Great set. Below are focused, interview-ready answers with practical code examples and trade-offs for each question.
Q1 — How do you split React components for both performance & maintainability?
Answer (short)
Split by responsibility (single responsibility), reusability, and render cost. Use a mix of small presentational components and larger container/feature components. Lazily load rarely-used pieces and avoid rerenders by isolating state.
Practical guidelines
-
Feature / domain folders: group components, hooks, styles by feature (e.g.,
/orders/,/cart/). Easier to navigate and change. -
Presentational vs Container: Presentational (UI-only) + Container (data/state) separation keeps pure UI components cheap to test and memoize.
-
Granularity: Break into small components only when it improves readability or avoids expensive re-renders; otherwise keep a small number of components to reduce overhead.
-
Single Responsibility: One component = one reason to change.
-
Avoid prop drilling: prefer context, custom hooks, or composition.
-
Split by render cost: Components that change frequently should be separated from ones that rarely change to reduce props causing re-renders.
-
Code-splitting:
React.lazy+Suspensefor route-level or heavy UI. -
Optimize expensive parts:
React.memo,useMemo,useCallbackonly where necessary (measure first). -
Hook extraction: Move logic to custom hooks to make components declarative and unit-testable.
Example folder structure
Micro-optimizations
-
Memoize child components that receive stable props.
-
Use lists with
keystable and minimal props. -
Use
shouldComponentUpdate/PureComponentpatterns orReact.memo. -
Keep expensive calculation outside render (useMemo, web worker).
Q2 — What happens internally when React reconciles a component tree?
Answer (short)
React performs a reconciliation (diff) to compute the minimal DOM updates. Modern React uses the Fiber architecture, which breaks work into units, supports priorities, interruption, and incremental rendering.
Key phases
-
Render (Reconciliation) phase
-
React builds a new fiber tree from JSX — pure, side-effect free.
-
Compares new tree vs current tree using heuristics:
-
If type is same, it updates props/state on the same fiber.
-
If type differs, it marks the old subtree for deletion and creates new fibers.
-
For lists, keys determine identity — keys prevent unnecessary re-creation.
-
-
Scheduling & priority (lanes) decide if this work can be interrupted.
-
-
Commit phase
-
Apply DOM mutations (insert/update/delete) in order.
-
Run lifecycle methods:
useLayoutEffectsync after DOM update, thenuseEffect(async). -
This phase is not interruptible.
-
Important internals / optimizations
-
Keys are critical for list stability; wrong keys cause re-create, losing state.
-
Fiber allows preemption — expensive renders can be split and yielded.
-
Bailing out: If
shouldComponentUpdate/memoindicates no change, React reuses fiber without traversing children. -
Hydration: server-rendered markup gets matched to fiber tree.
Example: list diff
-
Old:
[A(id=1), B(id=2), C(id=3)] -
New:
[A(id=1), C(id=3), D(id=4)]
React uses keys to move C and remove B, create D — minimal DOM work.
Q3 — How would you implement a global error boundary system for API & UI failures?
Answer (short)
Use React Error Boundaries for rendering errors + a global error handling layer for API failures (interceptors, context) and a centralized UI to show errors (toasts / modal). Combine with a logging/reporting service.
Key parts
-
UI Error Boundary: catches render-time errors in descendant tree.
-
Global API error interceptor: catch HTTP errors and route them to error handling context.
-
Error Context: central store to surface UI-friendly errors and allow retry.
-
Logging: send error events to Sentry / LogRocket / custom endpoint.
Implementation sketch
UX & robustness
-
Show friendly messages with retry buttons for transient errors.
-
Use error categories (network/timeouts/authorization) to decide fallback UI.
-
Provide global retry and refresh strategies (invalidate cache, re-auth).
-
Don’t use error boundaries to suppress errors silently — always log.
Q4 — How do you handle race conditions in React when multiple API calls fire simultaneously?
Answer (short)
Use cancellation (AbortController) and “stale-while-revalidate” patterns, sequence tokens, or last-wins semantics to ensure only the desired response is used.
Strategies
-
AbortController (recommended for fetch/axios): cancel previous request in
useEffectcleanup. -
Sequence tokens / request id: attach incremental id and ignore responses with outdated id.
-
Deduping in data layer: coalesce multiple identical requests so only one runs.
-
Optimistic UI with reconciliation: use latest-confirmed response to commit state.
-
Locking / semaphores: rarely used; if two operations conflict, queue or merge them.
Example: useEffect + AbortController
Example: sequence token
When to prefer what
-
If backend supports cancellation: use
AbortController. -
If network/cancellation not available: use tokens to ignore stale responses.
-
For identical concurrent requests: dedupe at hook or network layer.
Q5 — How would you build a custom hook for data fetching with caching + refetch logic?
Answer (short)
Build a hook similar to SWR / React Query: centralized cache store (Map), dedupe in-flight requests, TTL, stale-while-revalidate, manual refetch, and subscription for updates.
Core features
-
Cache by key
-
Deduplication of in-flight requests
-
TTL & stale-time
-
Background revalidation
-
Manual
refetch()andinvalidate() -
Error handling and retries
-
Abort support on unmount
Implementation (concise)
Usage
Production concerns
-
Add retries/backoff, request dedupe across tabs (BroadcastChannel), stale-while-revalidate semantics, and cache serialization (IndexedDB) for persistence.
-
Add concurrency limits and cancellation.
-
Expose optimistic updates and mutation helpers if writing.
Q6 — What would be your approach to rendering 10k+ DOM nodes efficiently (beyond just virtualization)?
Answer (short)
Virtualization is the primary approach but there are complementary and alternative techniques: DOM simplification, progressive rendering, canvas/WebGL/SVG, server-side rendering + pagination, DOM recycling, batching and idle-time rendering, and offloading work to workers.
Techniques & trade-offs
-
Virtualization + windowing
-
Only mount nodes visible in viewport. Use variable-height virtualization (cell measurement) if needed.
-
-
DOM simplification & lighter markup
-
Reduce node depth and remove unnecessary wrappers.
-
Avoid expensive CSS (box-shadow, complex filters).
-
Use simpler elements (divs vs heavy component tree).
-
Reuse CSS classes instead of inline styles.
-
-
DOM recycling (recycler pattern)
-
Reuse existing DOM nodes and update their content instead of creating/destroying nodes (useful for scrolling grids).
-
-
Canvas / WebGL / SVG
-
If nodes are mostly visual (lists, charts), render them on a
<canvas>or WebGL; far fewer DOM nodes, but lose accessibility and easy semantics.
-
-
Progressive / chunked rendering
-
Render items in chunks using
requestIdleCallbackorsetTimeout(...,0)to avoid long frames. -
Render a skeleton or top N items first, lazy-render rest.
-
-
Pagination & server-side aggregation
-
Don’t ask UI to show 10k at once — provide meaningful pagination, filtering, or search.
-
-
Offload heavy computation to Web Workers
-
Precompute layout or transform data off-main-thread; main thread only updates DOM with minimal diffs.
-
-
Reduce reflows & layout thrashing
-
Batch DOM writes and reads. Use
transformfor animations. -
Use CSS
contain: layoutandwill-changeto isolate layout.
-
-
Use virtualization + placeholder reuse
-
Combine virtualization with DOM recycling to keep the number of mounted elements bounded and reuse them.
-
-
Server-side rendering + static snapshots
-
Pre-render initial subset, then hydrate lazily. Avoid hydrating all 10k nodes at once.
-
Concrete pattern: chunked render example
When to use Canvas/WebGL
-
If list items are purely graphical (icons, shapes, not interactive controls), use canvas to draw thousands of elements quickly.
-
Trade-offs: less semantic DOM, accessibility and event handling complexity.
Other optimizations
-
Avoid heavy component trees per list item — flatten structure.
-
Use
requestAnimationFramefor animations andpassiveevent listeners for scroll. -
Use
IntersectionObserverfor image lazy loading. -
Monitor paint times and FPS using performance profiles — optimize based on real metrics.
TL;DR / Practical checklist
-
Split components by feature/responsibility and isolate expensive rendering with memoization + lazy-loading.
-
Reconciliation builds new fiber tree and applies minimal DOM changes; keys are crucial.
-
Global error handling = Error Boundaries (UI) + API interceptors + error context + logging + retries.
-
Race conditions solved with
AbortController, tokens, and deduping. -
Custom data hook should include caching, TTL, dedupe, manual refetch and subscription model (like simplified SWR).
-
10k+ nodes: combine virtualization with DOM simplification, recycling, chunked rendering, canvas/WebGL when appropriate, and server-driven pagination.
