Senior, staff, and principal JavaScript interviews are barely about 'what prints.' They're about what breaks at scale, how you'd know, and the trade-off you're knowingly accepting. Here's the real question set.
NK
Nicanor Korir
Author
July 15, 2026
22 min read
JavaScriptInterviewAdvancedSystem DesignPerformanceArchitectureFrontendWeb Development
By the time you're interviewing for senior, staff, or principal roles, the questions invert. Nobody's checking whether you can trace var hoisting, they assume that. What they're screening for is judgment at scale: given a real system with real constraints, what do you build, what breaks under load, how would you know, and what trade-off are you knowingly accepting?
I made this transition myself, from "can I write the code?" to "can I own the outcome?", and the interviews reflect it. A senior loop typically has a deep language/debugging round, a 45-60 minute frontend system-design round (design a typeahead, an infinite feed, a resilient data layer), and often an architecture-and-judgment conversation. Less "what prints", far more "what fails at p99 and how do you defend against it."
The questions below are the real ones. They're interactive, but at this level, don't just reveal and read. Try to structure a full answer out loud first: requirements, constraints, the design, the failure modes, the trade-off. That structure is the signal. A senior who jumps straight to a solution reads as junior; one who scopes first reads as someone who's shipped.
The interactive practice hub lets you drill these as flashcards, useful for the language/debugging round, though the system-design questions reward practising out loud with a whiteboard.
What the senior/principal loop is really testing
Frontend system design, requirements, constraints, and defensible trade-offs, not a memorised diagram.
Performance and reliability under load, main-thread budgets, cancellation, backoff, virtualization.
Technical leadership, how you decide, and how you'd explain that decision to a team.
The behaviours that separate a strong senior: you gather requirements and state constraints before designing, you quantify (main-thread budget, payload size, p95) instead of hand-waving, and you volunteer the failure modes and the trade-off you're accepting rather than waiting to be caught.
Frontend system design
The centrepiece of a senior loop. The interviewer names a component or feature and watches how you scope it. The single biggest differentiator is resisting the urge to code immediately, spend the first few minutes on requirements and constraints.
Q1
System design★★★System design round
Design a production autocomplete/typeahead component. Walk through requirements, then the client architecture and the failure modes you'd handle.
#system-design
#debounce
#cancellation
#cache
Start by scoping, not coding. Clarify: expected QPS and result latency, result count, keyboard-only accessibility, mobile, do stale results matter, is ranking client- or server-side. State a target, e.g. suggestions within ~100ms of a pause, p95.
Client architecture:
Debounce keystrokes (~150-300ms) so you query on pause, not per character.
Cancel in-flight requests with AbortController when a newer keystroke supersedes them, otherwise a slow old response can overwrite a fresh one (the out-of-order race).
Cache by query string (an LRU Map) so backspacing or repeating a prefix is instant and offline-friendly.
Race handling: tag each request with a sequence number and ignore any response older than the latest rendered query, belt-and-suspenders with cancellation.
Minimum query length and trimming to avoid useless calls.
UX & a11y: a combobox with proper ARIA roles, arrow-key navigation, aria-activedescendant, debounced live-region announcements, and visible loading/empty/error states.
Failure modes I'd name up front: network errors (show retry, keep last good results), the stale-response race, unbounded cache growth, and hammering the backend (debounce + min length + server rate limiting).
Follow-ups they may ask
Client-side vs server-side ranking, how do you decide?
How would you make results feel instant on a flaky connection?
Q2
Performance★★★System design round
A feed needs to show tens of thousands of rows and stay at 60fps while scrolling. How do you build it?
#virtualization
#rendering
#main-thread
You never render 10k DOM nodes, the browser can't lay them out or paint them in a 16ms frame. The core technique is windowing / virtualization: render only the ~visible rows plus a small overscan buffer, absolutely position them inside a tall spacer whose height equals totalRows * rowHeight, and swap contents as the user scrolls. That keeps the live node count constant regardless of dataset size.
Round it out with: variable row heights via measurement + a position cache; incremental data loading (infinite scroll with a sentinel + IntersectionObserver, plus request cancellation); keeping scroll-handler work tiny and off the critical path (passive listeners, requestAnimationFrame, avoid layout thrash by batching reads then writes); and offloading heavy transforms to a Web Worker so the main thread stays free. I'd frame it around a main-thread budget: measure with Performance panel / long-task API and keep per-frame work under budget.
Follow-ups they may ask
How do you handle rows whose height you don't know until render?
What causes layout thrashing and how do you avoid it in the scroll handler?
Q3
System design★★★System design round
Design a resilient data-fetching layer for a large app: caching, request de-duplication, retries, and cancellation. What are the trade-offs?
#fetch
#retry
#dedupe
#cache
Wrap fetch in a client that adds cross-cutting concerns so every call site gets them for free:
De-duplication: keep an in-flight Map keyed by request signature; concurrent identical requests share one promise. Prevents N components triggering N identical calls.
Caching with staleness: a keyed cache with TTL, and a stale-while-revalidate policy, serve cached data instantly, refetch in the background. Expose cache invalidation by tag.
Retries with backoff + jitter, but only for idempotent requests and retryable statuses (network errors, 5xx, 429 with Retry-After), never blindly retry a POST.
Cancellation via AbortController, wired to component lifecycle so unmounts abort.
Error normalisation so callers get a consistent error shape.
Trade-offs I'd call out: caching risks staleness (correctness vs speed, pick TTLs per resource); retries can amplify an outage (why jitter + caps + circuit-breaking matter); de-dup assumes requests are truly equivalent. This is essentially what React Query/SWR give you, I'd usually adopt one rather than rebuild it, and I'd say so.
Follow-ups they may ask
Which requests are safe to retry, and how do you know?
Stale-while-revalidate vs cache-then-network, when does each fit?
Async patterns & concurrency at scale
At this level, async questions aren't "what prints", they're "this races in production, why, and how do you make it correct." Cancellation, backoff, and the event loop's failure modes all show up here.
Q4
Async patterns★★★Live coding
Design a polling utility (or React hook) that fetches on an interval with exponential backoff on failure, jitter, and clean cancellation. What are the edge cases?
#polling
#backoff
#AbortController
#hooks
The shape: a loop that awaits a fetch, and on success resets the delay to the base and waits the normal interval; on failure multiplies the delay (capped) and retries.
js
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Key production details interviewers look for:
Exponential backoff with a cap so a struggling server isn't hammered, min(base * 2^n, max).
Jitter (randomise the delay) so many clients don't retry in a synchronised thundering herd.
`AbortController` to cancel the in-flight request on unmount/cleanup, otherwise you set state on a dead component and leak.
Pause when the tab is hidden (document.visibilityState) to save battery and quota.
Stop conditions, give up after N failures or surface the error.
Follow-ups they may ask
Where does the visibility-pause logic hook in?
How would you test the backoff timing deterministically?
Q5
Concurrency★★★Debugging round
Users report that a details panel sometimes shows the wrong record after clicking through a list quickly. What's happening and how do you fix it?
#race-condition
#async
#cancellation
This is a classic async race. Each click fires fetch(id) and renders when it resolves. If request A (slow) was fired before request B (fast) but resolves after it, A's response lands last and overwrites B, so the panel shows the record you clicked earlier, not the current selection. Responses aren't guaranteed to arrive in request order.
Fixes, in order of preference:
Cancel superseded requests with AbortController, when a new selection happens, abort the previous fetch.
Ignore stale responses: capture the current selection id (or a monotonically increasing request counter) in the closure and, when the response arrives, only render if it still matches the latest, otherwise drop it.
In React, a cleanup function that sets an ignore flag on effect re-run does the same thing.
I'd use cancellation and the stale-guard together, cancellation frees the network, the guard is the correctness backstop.
Follow-ups they may ask
Show the `useEffect` cleanup version of the stale-guard.
Does aborting guarantee the old `.then` never runs?
Q6
Event loop★★★Debugging round
Can Promise-based code freeze the UI even though 'promises are async and non-blocking'? Explain.
#event-loop
#microtask
#starvation
Yes. 'Non-blocking' refers to I/O, not CPU. The event loop drains the entire microtask queue before it renders a frame or handles the next task. So a chain of microtasks that keeps scheduling more microtasks, e.g. a recursive Promise.resolve().then(...) or a tight await loop over synchronous work, never lets the loop reach rendering or input. The tab is fully responsive in principle but visually frozen: this is microtask starvation.
The fixes: break heavy work into macrotasks (setTimeout(…, 0), MessageChannel, or scheduler.postTask) so the loop can render between chunks; use requestIdleCallback/requestAnimationFrame to cooperate with the frame; or move the CPU work to a Web Worker entirely. The general principle: keep any synchronous slice under a few milliseconds and yield the thread between slices.
Follow-ups they may ask
How would `scheduler.postTask` or `isInputPending` help you yield well?
Why doesn't wrapping the work in a Promise make it 'run in the background'?
Performance, memory & debugging
Senior engineers are expected to debug what juniors can't even see. The steadily-growing-memory question is a favourite because it separates people who've actually profiled a production leak from those who've only read about WeakMap.
Q7
Performance★★★Debugging round
A single-page app's memory grows steadily the longer it runs and eventually janks. How do you find and fix the leak?
#memory-leaks
#gc
#weakmap
Confirm and locate first. Take heap snapshots in DevTools Memory tab over time; a growing retained size and rising detached-DOM-node count point to a leak. Compare snapshots to see which constructor is accumulating, and use 'retainers' to find what's holding the objects alive.
Common culprits in SPAs:
Listeners/subscriptions not removed on unmount, the single most common one. Every addEventListener, interval, observer, or store subscription needs teardown.
Detached DOM nodes kept alive by a JS reference (a closure or array still pointing at a removed node).
Unbounded caches / arrays that only ever grow.
Closures pinning large objects longer than needed.
Fixes: pair every subscription with an unsubscribe (React cleanup, AbortController for listeners via signal), bound caches (LRU), and use `WeakMap`/`WeakSet` for metadata keyed by objects so entries are collected when the key object dies. Then re-snapshot to prove the growth is gone.
Follow-ups they may ask
Why does a `WeakMap` avoid the leak an object-keyed `Map` would cause?
How would you catch this class of bug in CI before it ships?
Security & accessibility as senior signals
Two areas interviewers use as clean seniority filters: whether you think about the security blast radius, and whether you understand accessibility as semantics-and-interaction rather than a checklist.
Q8
Security★★★Architecture round
Walk me through how XSS happens in a modern SPA and the layers of defence you'd put in place.
#xss
#csp
#sanitization
XSS is executing attacker-controlled script in a user's session, stealing tokens, acting as them. It happens when untrusted data reaches an HTML/JS sink: innerHTML, dangerouslySetInnerHTML, document.write, unsanitised URLs (javascript:), or injecting into inline event handlers.
Defence in depth:
Contextual output encoding, treat data as data. Frameworks like React auto-escape text by default; the danger is opting out (dangerouslySetInnerHTML).
Sanitize any HTML you must render with a vetted library (DOMPurify), never a hand-rolled regex.
Content Security Policy as a second wall, disallow inline scripts and restrict script origins, so even an injection struggles to execute. Use nonces/hashes.
`HttpOnly`, `Secure`, `SameSite` cookies so a successful XSS still can't read the session token from JS.
Validate/normalise URLs before using them in href/src.
Guard against prototype pollution and untrusted JSON/template inputs; keep dependencies patched (supply-chain).
The framing that lands: no single control is enough, encode by default, sanitize what must be HTML, and let CSP + cookie flags contain the blast radius.
Follow-ups they may ask
What is prototype pollution and how would it bite a frontend app?
Why isn't 'we use React so we're safe from XSS' sufficient?
Q9
Accessibility★★★System design round
Interviewers say accessibility is one of the cleanest ways to spot a senior frontend engineer. Why, and how do you build a complex widget (say a modal or combobox) accessibly?
#accessibility
#a11y
#semantics
It's a signal because a11y forces you to understand semantics and interaction states, not just visual layout, juniors style a div to look like a button; seniors reach for the right element and know the keyboard and screen-reader contract.
For a modal: use <dialog> or a container with role="dialog" + aria-modal, move focus into it on open and trap focus within it, restore focus to the trigger on close, close on Escape, and mark the rest of the page inert so the screen-reader and Tab don't wander out. For a combobox: the WAI-ARIA Authoring Practices pattern, role="combobox", aria-expanded, aria-controls, arrow-key navigation with aria-activedescendant, and announcing result counts via a polite live region.
The senior instincts: start from semantic HTML (it gives you keyboard and roles for free), only reach for ARIA to fill genuine gaps ('no ARIA is better than bad ARIA'), test with keyboard-only and a screen reader, and respect prefers-reduced-motion. It's a product-quality signal, not a checkbox.
Follow-ups they may ask
What does 'no ARIA is better than bad ARIA' mean in practice?
How would you enforce a11y so it doesn't regress across a big team?
Architecture & technical judgment
The final piece is rarely about code at all. It's about how you make decisions that other teams and future-you have to live with, state architecture, API design as a contract, and the judgment to sometimes choose the boring option.
Q10
Architecture★★★Architecture round
How do you decide on a state-management approach for a large, multi-team frontend? What are the trade-offs?
#state-management
#architecture
#scale
I start by separating kinds of state, because they want different tools:
Server cache state (data from APIs), the bulk of most apps. This belongs in a data-fetching cache (React Query/SWR): it handles caching, revalidation, dedup, and staleness far better than hand-rolled global state. Conflating server data with UI state is the most common architectural mistake.
Global UI/client state (theme, auth session, cross-cutting flags), a lightweight store (Zustand/Redux Toolkit/Context) sized to actual sharing needs.
Local component state, keep it local; don't hoist to global 'just in case'.
URL state (filters, tabs, pagination), the URL is shared, linkable state; put it there.
Trade-offs to name: a single global store is simple but couples teams and re-renders; too many small stores fragment and duplicate; Context re-renders all consumers on any change, so it's for low-frequency values. For multi-team scale I'd emphasise clear ownership boundaries (feature-scoped stores), a normalised server cache, and avoiding a giant shared blob everyone mutates. The decision framework matters more than the library.
Follow-ups they may ask
Why is treating server data as 'just global state' a trap?
When does Context become a performance problem, and what's the fix?
Q11
Architecture★★★Architecture round
You're designing a shared component library / SDK used by many teams. How do you design the public API and evolve it without breaking consumers?
#api-design
#backward-compat
#library
API design principles: make the common case a one-liner and the complex case possible; prefer a small, orthogonal surface over many overlapping options; design for the call site (how it reads where it's used), not the implementation; sensible defaults so most users configure nothing; and fail loudly with clear errors. For components: controlled/uncontrolled done right, composition over a giant props bag, and accessibility baked in, not bolted on.
Evolution without breakage: treat the public surface as a contract under semver. Add, don't mutate, new optional params and new components are non-breaking; changing behaviour or removing anything is a major. Deprecate before removing: mark it, warn in dev, document the migration, and give a window (and ideally a codemod). Keep internal APIs clearly separated from public ones so you can refactor freely. Version the docs, and treat a breaking change as an org-wide event with a migration guide, the cost is other teams' time, so the bar is high.
Follow-ups they may ask
How would you ship a breaking change across 40 consuming teams?
Controlled vs uncontrolled components, how do you support both cleanly?
Q12
Technical judgment★★★Architecture round
Tell me about a time you chose NOT to use a popular tool or pattern, or deliberately picked the 'boring' solution. Walk me through the decision.
#trade-offs
#leadership
#decision-making
This is a judgment question, they're screening for whether you optimise for the system and the team, not for novelty. A strong answer has a shape:
The context and constraints, team size and experience, timeline, the actual scale (not imagined scale), maintenance reality.
The options and their real costs, including the seductive one you didn't pick, and why its costs (learning curve, operational burden, lock-in, complexity budget) outweighed its benefits here.
The decision and the trade-off you knowingly accepted, 'I chose the boring, well-understood option; I traded some raw performance/elegance for reliability and a team that can maintain it at 2am.'
How it played out, and what you'd revisit if constraints changed.
The meta-signal interviewers want: you have a framework for 'it depends', you can name what it depends on (scale, team, reversibility, blast radius) rather than defaulting to the shiniest tool. Principal engineers are hired for judgment, and boring-on-purpose is often the senior move.
Follow-ups they may ask
How do you weigh a decision that's hard to reverse vs easy to reverse?
How do you bring a skeptical team along on the 'boring' choice?
How to prepare for the senior/principal loop
Preparation here is different, you can't cram system design, and reciting answers backfires. What actually helps:
Practise the design framework out loud. For any prompt: (1) clarify requirements and constraints, (2) state a measurable target, (3) design the happy path, (4) then attack it with failure modes, (5) name the trade-off. Do this on a whiteboard until the structure is automatic.
Have real stories ready. The judgment questions want a system you actually shipped, a leak you actually found, a boring decision you actually defended. Prepare three or four with the context → options → decision → outcome shape.
Quantify everything. Replace "it'll be fast" with "the main-thread budget per frame is ~16ms, so I'd keep the scroll handler under X and move Y to a worker." Numbers read as senior.
Know your failure modes cold. Cancellation, retries with jitter, stale-response races, unbounded caches, XSS, memory leaks, these are the vocabulary of a senior answer.
Green flags
You gather requirements and state constraints before designing.
You quantify, budgets, payloads, percentiles, instead of hand-waving.
You volunteer failure modes and the trade-off you're accepting.
Red flags
Jumping to a solution before scoping the problem.
Designing the happy path only, no cancellation, retries, or error states.
"It depends" with no framework for what it depends on.
Where to go next
If the language/debugging questions here felt thin, the intermediate guide drills the closures, event loop, and implement-it-yourself utilities that the senior deep-dive round still expects you to nail cold. And the beginner guide is a fast confidence check on fundamentals.
To drill the whole series, beginner to principal, as timed flashcards with progress tracking, head to the interview practice hub.
Stay in the Loop
Get occasional updates on AI engineering, robotics projects, and lessons from building startups. No spam, unsubscribe anytime.