Engine Internals • Memory • Async Corners • Security • Architecture • Leadership • 2026

JavaScript Interview Questions for 10+ Years Experience (Senior)

JavaScript interviews for 10+ years of experience skip definitions like closures and go after what only experience teaches: how the engine really keeps objects and memory, where the obvious answer is wrong, which language corners cause rare production bugs, how you keep a big front end fast and safe across many teams, and how you lead people through hard calls. It is written for JavaScript engineers with around eight to fifteen years behind them, interviewing for senior, staff or lead roles. Each question shows what the interviewer is really checking, a shape for your answer and a sample you can adapt. Read the sample, then say your own version out loud with a story from your own work.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Memory 2 questions

Hard Technical round Senior Practice question

1. A heap snapshot shows a huge array kept alive by a small callback that never mentions that array. How can a closure hold on to a variable it doesn't use?

What the interviewer is really testing:
Whether you know that in V8 closures from the same scope share one context object, so one closure's captures can be kept alive by another.
Answer frame:

Shared context: variables that any inner function uses go into one context object shared by every closure made in that scope.

The leak: a long-lived closure keeps the whole context, including data only a short-lived sibling needed.

Fix: clear the big variable after use, or create the long-lived function in a scope that never sees it.

Sample spoken answer:

“In V8, when a function creates inner functions, every variable that any of them uses goes into one shared context object, and all the closures from that scope point at it. So if one inner function reads a big array and another inner function is the one that lives forever, say a timer or a listener, the long-lived one keeps the whole context alive, array included, even though it never touches it. The heap snapshot shows it clearly once you follow the retainers: the array is held by a context, and the context is held by the callback. The fix is either to set the big variable to null once the short-lived work is done, or to move the long-lived function into a separate helper so it's created in a scope that never captures the array. I've found this pattern behind slow leaks in dashboards that stay open all day.”

Code:
function setup(el) {
  const rows = loadHugeArray();
  const report = () => console.log(rows.length); // puts rows in the shared context
  report();
  el.addEventListener('click', () => el.classList.toggle('open'));
  // the click handler shares that context, so rows lives as long as the listener
}
Red flag to avoid:

Insisting a closure can only keep the variables it names, without ever checking the retainer path in a snapshot.

They may ask next:
  • How would you prove this is the retainer, rather than some other reference to the array?
  • Why can eval inside a function make the engine keep every variable in scope alive?
Say it in 60 seconds
Hard Technical round Senior Practice question

2. A teammate wants to use FinalizationRegistry to close database handles and sockets when the wrapper objects get garbage collected. What's your view?

What the interviewer is really testing:
Whether you know garbage collection gives no timing guarantee, so finalizers can't own important cleanup.
Answer frame:

No promise: the collector decides when, or whether, a callback runs; it may never run.

So: finalizers can't own scarce resources like handles, locks or sockets.

Right tool: explicit close or dispose methods, with try/finally around them.

Where it fits: caches of recomputable data, and a safety net that logs leaked handles.

Sample spoken answer:

“I'd say no for that use. Garbage collection gives no timing guarantee. A finalization callback can run much later than you expect, or never, for example if memory never gets tight or the process exits first. So if closing a socket depends on it, we'll run out of sockets on a quiet day and nobody will know why. Scarce resources need explicit ownership: a close or dispose method, called in a finally block, or tied to a clear lifecycle like a request or a component unmounting. Where FinalizationRegistry does help is as a safety net. I'd register the wrapper and, if the callback ever fires for a handle that was never closed, log a warning with where it was opened. That turns a silent leak into a bug report. WeakRef has a similar place: caches of things you can rebuild, never something you need to exist.”

Red flag to avoid:

Treating garbage collection like a destructor that runs at a known time.

They may ask next:
  • Why might a WeakRef target stay alive longer than you expect right after you read it?
  • How would you find every place in a big codebase that opens a handle but never closes it?
Say it in 60 seconds

Engine Internals 3 questions

Hard Technical round Senior Practice question

3. A hot function that reads the same few properties got much slower after a change that deletes a property on some objects and builds others with fields in a different order. Why?

What the interviewer is really testing:
Whether you understand object shapes and inline caches, and how code that looks harmless can push a property access onto the slow path.
Answer frame:

Shapes: the engine gives objects built with the same properties in the same order a shared hidden shape.

Inline caches: each property access remembers the shapes it has seen; one or a few is fast, many is slow.

What breaks it: different field orders make new shapes, and delete often turns an object into a slow dictionary.

Fix: set every field in the constructor in one order, and set to undefined instead of deleting; measure first.

Sample spoken answer:

“Engines like V8 don't look up properties by name every time. Objects that get the same properties in the same order share a hidden shape, and each property access site caches where the field lives for the shapes it has seen. With one shape that site is very fast, with a few it's still fine, but once it sees many shapes it gives up and does a slower generic lookup. Building objects with fields in different orders creates extra shapes, and delete is worse: it often switches that object to a dictionary mode that's slow to read. So the change quietly made a hot read site see lots of shapes. My fix would be to create every field up front in the constructor, always in the same order, and to set a field to undefined rather than delete it. I'd confirm it in the profiler first, because most code never needs this level of care.”

Red flag to avoid:

Treating delete and property order as free, or rewriting everything for shapes without a profile showing the hot spot.

They may ask next:
  • Why can a sparse array with holes be slower to read than a packed one?
  • How would you write a benchmark for this that doesn't mislead you?
Say it in 60 seconds
Hard Technical round Senior Practice question

4. You wrap a Map in a Proxy to log every access, and calling get on it throws a TypeError about an incompatible receiver. Why, and how do you fix the proxy?

What the interviewer is really testing:
Whether you know that built-ins and private fields depend on internal slots the proxy doesn't have, and how the receiver changes things.
Answer frame:

Internal slots: Map methods check that this is a real Map; the proxy isn't one.

Same trap: Set, Date, and classes with private fields fail the same way.

Fix: in the get trap, read from the target and bind methods to the target.

Cost: returned values may escape the proxy, and identity checks see two objects.

Sample spoken answer:

“Map stores its entries in an internal slot, not in normal properties. When you call proxy.get, the method is found through the proxy, but it runs with this set to the proxy, and the proxy doesn't have that slot, so the engine throws. The same happens with Set, Date, and any class that uses private fields, because reading a private field on the proxy fails too. The fix is to make the get trap read the property from the target, using the target as the receiver, and bind any function it returns to the target. Getters like size need that receiver as well. It's worth knowing the trade-off: a method like set returns the real map, so callers can end up holding the unwrapped object, and anything keyed by identity, like a WeakMap, sees the proxy and the target as different objects.”

Code:
const broken = new Proxy(new Map(), {});
// broken.get('a') -> TypeError

const logged = new Proxy(new Map(), {
  get(target, prop) {
    console.log('read', String(prop));
    const value = Reflect.get(target, prop, target);
    return typeof value === 'function' ? value.bind(target) : value;
  },
});
logged.set('a', 1);
logged.get('a'); // 1
Red flag to avoid:

Assuming a Proxy is a transparent stand-in for any object.

They may ask next:
  • Why doesn't a has trap intercept calls to map.has?
  • When would you choose a subclass or a plain wrapper object over a Proxy?
Say it in 60 seconds
Hard Technical round Senior Practice question

5. Error handling checks err instanceof ApiError, and in one part of the app it's false even though the error clearly is an ApiError. Arrays from an iframe fail instanceof Array too. What's the common cause?

What the interviewer is really testing:
Whether you know instanceof compares against one specific constructor, and that realms and duplicate package copies break it.
Answer frame:

How instanceof works: it walks the prototype chain looking for one exact prototype object.

Realms: each iframe or worker has its own Array and Error constructors.

Duplicate packages: two copies of a library in the bundle mean two ApiError classes.

Fix: Array.isArray, brand checks by name or code, and dedupe the dependency.

Sample spoken answer:

“instanceof doesn't check a type name. It walks the object's prototype chain looking for one specific prototype object. An iframe is a separate realm with its own global Array, so an array made there isn't an instance of our Array, which is why Array.isArray exists. The ApiError case is usually the same idea from the bundler: two versions of the shared package ended up in the bundle, so there are two ApiError classes, and code that imports one copy rejects errors built by the other. I'd confirm with a bundle analyzer, then dedupe so one version is installed. For the long run I'd stop relying on instanceof across package boundaries and check a stable field like name or code instead. Duplicate copies also mean duplicate module state, like two caches or two event buses, which is often the bigger hidden bug.”

Red flag to avoid:

Calling instanceof unreliable in general, without knowing it compares one exact prototype object.

They may ask next:
  • What does Symbol.hasInstance let a class do, and would you use it here?
  • How would you stop duplicate package versions creeping back into the bundle?
Say it in 60 seconds

Edge Cases 3 questions

Medium Technical round Mid-level, Senior Practice question

6. The API sends products in ranked order as an object keyed by product ID. In the UI the ranking is lost and items come out sorted by ID. The JSON text looks right. What's going on?

What the interviewer is really testing:
Whether you know the property order rules for objects, where keys that look like array indexes always come first in number order.
Answer frame:

The rule: keys that look like array indexes come first in ascending number order, then other string keys in the order added, then symbols.

The bug: numeric IDs count as index-like keys, so the parsed object lists them by number, not in the order sent.

Fix: order belongs in an array; for a keyed collection that keeps insertion order for any key, use a Map.

Contract: JSON objects are unordered by definition, so fix the API shape, not the client loop.

Sample spoken answer:

“Object property order is defined, just not the way most people expect. Keys that look like array indexes, meaning plain non-negative whole numbers like '7' or '1042', always come first, in ascending numeric order. Other string keys follow in the order they were added, and symbols come last. Our product IDs are numbers, so once JSON.parse builds the object, Object.keys and for...in walk them by ID, and the ranking is gone even though the raw text was in the right order. An ID with a leading zero, like '007', isn't index-like, so it keeps its sent position after the numeric ones, which makes the bug look random. The real fix is the contract: JSON objects are unordered by definition, so order goes in an array of items, or an array of IDs next to the lookup object. On the client, when I need a keyed collection that keeps insertion order for any key, I use a Map.”

Code:
const ranked = JSON.parse('{"30": "c", "4": "a", "12": "b", "x": "z"}');
Object.keys(ranked); // ['4', '12', '30', 'x']

const order = new Map([['30', 'c'], ['4', 'a'], ['12', 'b']]);
[...order.keys()];   // ['30', '4', '12']
Red flag to avoid:

Saying object keys have no order at all, or that they always keep insertion order.

They may ask next:
  • Does JSON.stringify write keys out in the order you inserted them?
  • Where else in a large codebase could this ordering rule cause a quiet bug?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

7. A table sorted correctly for years, then after a browser update the order came out scrambled. The comparator is (a, b) => a.price > b.price. What's wrong?

What the interviewer is really testing:
Whether you know the comparator contract, and that a comparator breaking it gives results that depend on the engine, not on your code.
Answer frame:

Contract: the comparator must return a negative number, zero or a positive number, and agree with itself on every pair.

The bug: a boolean becomes 1 or 0, never negative, so the result depends on the sort algorithm.

Why now: the engine changed its sort algorithm; the code was always wrong.

Same family: a comparator that can return NaN, for example on a missing price, breaks the contract too.

Sample spoken answer:

“The comparator has to return a negative number when a comes first, a positive number when b comes first, and zero when they're equal, and it has to agree with itself for every pair. A boolean turns into 1 or 0, so it never says 'a comes first'. With a comparator like that, the spec doesn't define the result; it depends on which algorithm the engine uses. The old algorithm happened to give the right order for our data, and when the engine switched to a different one, the same code produced a different order. So the code was always wrong and just got lucky. The fix is a.price minus b.price. I'd also check what happens when a price is missing, because subtraction then gives NaN, which the sort treats as equal, and that breaks the contract in a quieter way. So missing values get an explicit place, first or last.”

Code:
items.sort((a, b) => a.price > b.price);   // wrong: never returns a negative
items.sort((a, b) => a.price - b.price);   // right, if every price is a number

// missing prices go last, on purpose
items.sort((a, b) => (a.price == null) - (b.price == null) || a.price - b.price);
Red flag to avoid:

Calling it a browser bug, or patching it with a second sort instead of fixing the comparator contract.

They may ask next:
  • Why is sorting with a comparator that returns Math.random() - 0.5 a bad way to shuffle?
  • How would you catch comparator bugs like this in tests, before an engine change finds them for you?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

8. A 30-character limit on display names lets some names through that look shorter, rejects others that look fine, and sometimes cuts a name into a broken symbol. Why?

What the interviewer is really testing:
Whether you know strings are UTF-16 code units, and the difference between code units, code points and what a user sees as one character.
Answer frame:

Code units: length counts UTF-16 units; many emoji take two.

Code points: spreading a string or for...of splits by code point, still not what users see.

Graphemes: accents, flags and combined emoji are several code points; Intl.Segmenter counts what users see.

Consistency: normalise, and agree the rule with the server, which may count bytes.

Sample spoken answer:

“JavaScript strings are sequences of UTF-16 code units, and length counts those units. Many emoji sit outside the basic range and take two units, so they count double, and slicing at a fixed index can cut one in half and leave a broken symbol. Spreading the string or using for...of splits by code point, which fixes that case but not all of them: an accented letter can be a base letter plus a combining mark, and flags or family emoji are several code points joined together. What users think of as one character is a grapheme, and Intl.Segmenter with grapheme granularity counts those. So I'd normalise the text to NFC first, count and cut by graphemes in the UI, and agree the rule with the backend, because a database column often limits bytes, and that's a different number again.”

Code:
const name = 'e\u0301';                  // shows as one accented letter
name.length;                            // 2
'\u{1F600}'.length;                      // 2, but [...'\u{1F600}'].length is 1
const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
[...seg.segment(name)].length;          // 1
name.normalize('NFC').length;           // 1
Red flag to avoid:

Assuming length is the number of characters the user sees.

They may ask next:
  • Why can two names that look identical fail an equality check?
  • How would you truncate a long title safely for a preview?
Say it in 60 seconds

Async Corners 4 questions

Hard Technical round Mid-level, Senior Practice question

9. An async function wraps its work in try/catch with a fallback, yet the caller still gets the rejection and the fallback never runs. The line is return fetchOrder(id). Why?

What the interviewer is really testing:
Whether you know that returning a promise inside try hands it back before it settles, so the catch and finally don't cover its failure.
Answer frame:

What happens: return hands the pending promise out and leaves the try block at once.

Result: a later rejection skips the catch, and finally runs before the work is done.

Fix: use return await inside try, catch and finally blocks.

Bonus: await keeps the function in async stack traces.

Sample spoken answer:

“When you write return fetchOrder(id) inside try, the function doesn't wait for that promise. It returns it right away and leaves the try block, so the try is already finished when the request fails. The rejection goes straight to the caller, the catch never sees it, and any finally block runs before the work has even completed, which is nasty if finally releases a lock or hides a spinner. Writing return await fetchOrder(id) makes the function wait inside the try, so the catch and finally behave the way the code reads. Outside a try block the two are almost the same, but I still prefer await because engines like V8 can then show this function in async stack traces, and that saves real time in production debugging. The old worry about an extra tick isn't worth a bug like this one.”

Code:
async function loadOrder(id) {
  try {
    return await fetchOrder(id); // without await, the catch below never runs
  } catch (err) {
    return cachedOrder(id);
  }
}
Red flag to avoid:

Saying return await is always redundant, without knowing how it changes try, catch and finally.

They may ask next:
  • How would you enforce this rule across a large codebase instead of relying on review?
  • What happens if an async function returns a thenable object that isn't a real promise?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

10. A caching helper calls its callback right away on a cache hit but later on a miss. Bugs appear only once the cache is warm. Why is that design dangerous, and how do you fix it?

What the interviewer is really testing:
Whether you know an API must be always synchronous or always asynchronous, because otherwise the order of the caller's code changes from call to call.
Answer frame:

The problem: callers cannot know whether their code after the call runs before or after the callback.

Warm-cache bugs: on a hit the callback runs mid-call and sees state the caller hasn't set up yet.

Fix: always be asynchronous, for example return a promise even for a cached value.

Rule: put it in the team's API guidelines and review checklist.

Sample spoken answer:

“A function that's sometimes synchronous and sometimes asynchronous makes the order of the caller's code unpredictable. On a cache miss, the callback runs later, so everything after the call, like setting a loading flag or storing a request ID, has already happened. On a hit, the callback runs in the middle of the call, before that code, so it sees half-set-up state. The cold path gets all the testing, so the bug only shows up in production once the cache is warm, and it's hard to reproduce. The fix is to make it always asynchronous. The simplest way is to return a promise and resolve it with the cached value, because then the caller's then or await always continues after the current code finishes. If it has to stay a callback API, I defer the hit with queueMicrotask. One extra tick is a small price for code that always runs in the same order.”

Code:
const cache = new Map();

function getUser(id) {
  if (cache.has(id)) return Promise.resolve(cache.get(id)); // still async for the caller
  return fetchUser(id).then((user) => {
    cache.set(id, user);
    return user;
  });
}
Red flag to avoid:

Calling the synchronous cache-hit path a free speed-up, without seeing that it changes the order code runs in.

They may ask next:
  • Two callers ask for the same missing user at the same moment. How do you stop two requests going out?
  • When is it fine for an API to be purely synchronous instead?
Say it in 60 seconds
Hard Technical round Senior Practice question

11. To stop a long loop freezing the page, someone split it into a promise chain that handles one item per step. The page still freezes until it's done. Why, and what would you do instead?

What the interviewer is really testing:
Whether you know the whole microtask queue runs before the browser can render or handle input, so promises alone never give the page a turn.
Answer frame:

Microtasks drain fully: after each task, every queued microtask runs, including ones added along the way, before rendering or input.

So: a chain of promise callbacks is still one long block for the page; it only looks asynchronous.

Yield for real: work in chunks and schedule each next chunk as a new task, such as setTimeout or a MessageChannel message.

Or move it: work that is heavy even in chunks belongs in a worker.

Sample spoken answer:

“Promise callbacks are microtasks, and the event loop runs the whole microtask queue after each task before it moves on, including microtasks added while the queue is draining. So when each step of the chain schedules the next one, the queue never empties, and the browser can't paint or handle a click until the last item is done. It looks asynchronous in the code, but to the page it's one long block, and it's often slower than the plain loop. To really give the page a turn, I process a chunk of items, then schedule the next chunk as a new task with setTimeout or a MessageChannel message, so rendering and input can run in between. If the work still hurts in chunks, it goes into a worker. Then I'd confirm the fix in a performance trace by checking that the long tasks are gone.”

Code:
// still freezes: handle is synchronous, every step is a microtask
items.reduce((p, item) => p.then(() => handle(item)), Promise.resolve());

// gives the page a turn: each chunk is a new task
function processInChunks(items, start = 0) {
  const end = Math.min(start + 200, items.length);
  for (let i = start; i < end; i++) handle(items[i]);
  if (end < items.length) setTimeout(() => processInChunks(items, end), 0);
}
Red flag to avoid:

Believing that wrapping CPU-heavy work in promises or async functions lets the browser render in between.

They may ask next:
  • Why does an async function that awaits inside a loop of synchronous work not help either?
  • How would you pick the chunk size, and how would you know it's working for users on slow phones?
Say it in 60 seconds
Hard Technical round Senior Practice question

12. Someone added top-level await in a shared config module to fetch settings at load, and now the whole app starts slower, and one failed request leaves a blank page. Why?

What the interviewer is really testing:
Whether you understand how top-level await changes module evaluation for everything that imports the module.
Answer frame:

Evaluation waits: a module with top-level await pauses, and every module that imports it waits too.

Failure spreads: if the await rejects, the module fails, and so does every importer.

Fix: export an init function or a promise, and await it at the entry point with a fallback.

Rule: keep top-level await to entry points and truly one-off scripts.

Sample spoken answer:

“Top-level await makes that module asynchronous in the module graph. It can't finish evaluating until the await settles, and every module that imports it, directly or indirectly, waits before its own code runs. Modules that don't depend on it can still go ahead, but a shared config module is imported by almost everything, so the whole app waits on one network request before any of it runs. And if that request fails, the module fails to evaluate, the error spreads to all its importers, and you get a blank page instead of a nice error screen. I'd change the config module to export a loadConfig function or a promise, and await it once at the entry point, where we can show a loading state, retry, or fall back to defaults. My team rule is that top-level await belongs in entry points and small scripts, never in shared library modules.”

Red flag to avoid:

Treating top-level await as a local convenience without seeing that it delays every importer.

They may ask next:
  • How does top-level await interact with two modules that import each other?
  • Why can't a classic script, as opposed to a module, use top-level await?
Say it in 60 seconds

Browser Runtime 2 questions

Medium Technical round Mid-level, Senior Practice question

13. A countdown built with setInterval runs slow on some machines and falls far behind when the tab is in the background. Why, and how would you build it properly?

What the interviewer is really testing:
Whether you know timers only promise a minimum delay, that browsers throttle them, and that time must come from a clock, not a tick count.
Answer frame:

Timers are a minimum: callbacks wait for a free main thread, and nested timers get clamped.

Background tabs: browsers throttle timers heavily to save power.

Fix: store the end time and compute what's left from a clock on each tick.

Resync: refresh on visibilitychange, and take the end time from the server if it matters.

Sample spoken answer:

“A timer delay is a minimum, not a promise. The callback only runs when the main thread is free, so a busy page delays every tick, and browsers clamp deeply nested timers to a small minimum. In background tabs they throttle timers much harder to save battery. So a countdown that subtracts one second per tick drifts, and in a hidden tab it can fall way behind. The fix is to stop counting ticks. I store the end time once, and every tick computes the remaining time from the clock, so a late tick just shows the right value late instead of the wrong value. I'd use performance.now for elapsed time because it doesn't jump when the system clock changes, listen for visibilitychange to redraw straight away, and if the deadline matters, say an auction, the end time comes from the server.”

Red flag to avoid:

Trying to fix drift with a smaller interval, or assuming setInterval fires exactly on time.

They may ask next:
  • Why is requestAnimationFrame a poor choice for keeping time in a background tab?
  • How would you keep a clock in sync with the server when the user's device clock is wrong?
Say it in 60 seconds
Hard Technical round Senior Practice question

14. You moved heavy JSON processing into a Web Worker to keep the UI smooth, and the total time got worse. What's likely going on?

What the interviewer is really testing:
Whether you know postMessage copies data by structured clone, and how to avoid paying that cost twice.
Answer frame:

Copy cost: postMessage structured-clones the data, on the way in and on the way out.

Where time goes: cloning a big object graph can cost more than the work itself, some of it on the main thread.

Fixes: send raw text or bytes and parse in the worker, transfer ArrayBuffers, keep data in the worker.

Reuse: start one worker and keep it, instead of one per task.

Sample spoken answer:

“postMessage doesn't share memory. It copies the data using structured clone, so if the main thread parses a big response and then posts the resulting objects, we pay to parse, then to clone on the main thread, then to rebuild the copy in the worker, and the same again for the result. For large object graphs the copying can cost more than the processing. The first fix is to move the whole job: fetch inside the worker, or pass the raw text, and parse there. For binary data you can transfer an ArrayBuffer instead of copying it, which moves ownership with no copy, and the sender's buffer is left empty. Better still, keep the dataset living inside the worker and only send small queries and small results across. I'd also keep one long-lived worker, since starting one per task adds its own delay. Then I'd check the main thread in a performance trace, because that's what the user actually feels.”

Red flag to avoid:

Assuming a worker makes any heavy job faster, without accounting for the copy across the boundary.

They may ask next:
  • What does SharedArrayBuffer need from your server headers before a page can use it?
  • Which kinds of values can't be sent with postMessage at all?
Say it in 60 seconds

Security 3 questions

Hard Technical round Senior Practice question

15. Your page and a payment iframe on another domain talk through postMessage. In code review, what exactly do you check on the sending side and on the receiving side?

What the interviewer is really testing:
Whether you know postMessage crosses the origin boundary on purpose, so both sides must pin the exact origin and treat the data as untrusted.
Answer frame:

Receiving: compare event.origin to an exact allowed origin, and where it matters check event.source is the expected frame.

Sending: pass the exact target origin, never '*', so the data can't reach a different site loaded in that frame.

Data: treat every message as untrusted input: check its type and fields, never feed it to innerHTML or code that runs it.

Scope: keep tokens out of messages where a server-side check can do the job, and remove listeners you no longer need.

Sample spoken answer:

“postMessage is built to cross origins, so any window that can get a reference to ours can send us a message, and our messages go wherever we point them. On the receiving side, the listener must compare event.origin to an exact allowed origin, not with indexOf or startsWith, because an attacker can register a domain that starts with ours. Where it matters I also check that event.source is the iframe's contentWindow. On the sending side, I pass the exact target origin instead of a star. Then if the frame has been navigated to another site, the browser drops the message instead of handing over our data. After that, every message is untrusted input: I check the type and fields against what we expect and never put it into innerHTML or anything that runs code. And I keep tokens out of messages when the server can confirm the payment itself.”

Code:
const PAY_ORIGIN = 'https://pay.example.com';

window.addEventListener('message', (event) => {
  if (event.origin !== PAY_ORIGIN) return;
  if (event.source !== frame.contentWindow) return;
  const msg = event.data;
  if (!msg || msg.type !== 'payment-result' || typeof msg.id !== 'string') return;
  handlePaymentResult(msg.id);
});

frame.contentWindow.postMessage({ type: 'start', orderId }, PAY_ORIGIN);
Red flag to avoid:

Sending with '*' as the target origin, or skipping the origin check because the iframe is 'ours'.

They may ask next:
  • What does event.origin look like if the iframe is sandboxed without allow-same-origin, and how does that change your check?
  • How would you test this handler against the attacks you are worried about?
Say it in 60 seconds
Hard Technical round Senior Practice question

16. A form validator freezes the whole page for seconds on certain inputs, and the same regex takes down a Node service when a user pastes a long string. What's going on?

What the interviewer is really testing:
Whether you recognise catastrophic backtracking, why it blocks the single thread, and how to defend against it.
Answer frame:

Cause: a backtracking regex engine with nested or overlapping quantifiers tries a huge number of ways to match.

Why it hurts: the match runs on the one main thread or event loop, so everything stops.

Fix: rewrite the pattern so each character can match only one way, and cap the input length.

Defend: test patterns with worst-case inputs, and move risky matching off the main thread.

Sample spoken answer:

“JavaScript regex engines backtrack. With a pattern like a repeated group that itself contains a repeat, and pieces that can match the same characters, a string that almost matches makes the engine try an exploding number of ways to split it before it gives up. That can run for seconds or much longer. Because JavaScript runs on one thread, the page freezes, and on a Node server one request blocks every other request, which is why it's a real denial-of-service risk. The fix is to rewrite the pattern so there's only one way to match each character, removing nested quantifiers and overlapping alternatives, and to cap input length before matching. I'd add worst-case strings to the tests for every validator. For patterns that come from users or config, I'd run them in a worker with a time limit, or use a linear-time regex engine on the server.”

Code:
const risky = /^(\w+\s?)*$/;
risky.test('a'.repeat(30) + '!'); // can run for a very long time

const safer = /^\w+(\s\w+)*\s?$/; // each character can match only one way
Red flag to avoid:

Saying regexes are always fast, or trying to fix it with a timeout on the main thread.

They may ask next:
  • Why is a string that almost matches the worst case, rather than one that clearly doesn't?
  • How would you review every regex in a large codebase for this without reading them all by hand?
Say it in 60 seconds
Hard Situational round Senior Practice question

17. News breaks that a popular npm package your apps depend on published a malicious version yesterday. You're leading the response. What do you do in the first few hours?

What the interviewer is really testing:
Whether you can run a supply-chain incident calmly: find exposure, contain it, rotate secrets, then fix the process.
Answer frame:

Exposure: search lockfiles and CI logs for the bad version in every app and build since it was published.

Contain: pin a known good version, block the bad one, rebuild from clean caches, check deployed bundles.

Assume access: install scripts can run code, so rotate secrets from affected CI runners and laptops.

Afterwards: enforce lockfiles, limit install scripts, delay brand-new versions, and write it up.

Sample spoken answer:

“First, exposure. I'd get a small group together and search every lockfile and CI log for that version since it was published, so we know which apps, builds and developer machines pulled it. Then containment: pin the last good version, block the bad one in our registry proxy, clear caches and rebuild. If the bad code could have reached the browser, I'd check what we actually deployed and roll back if needed. Because package install scripts can run any code, I'd treat affected CI runners and laptops as compromised and rotate the tokens and keys they could see. Only then do the write-up and the process fixes: installs only from the lockfile in CI, install scripts turned off unless a package is on an allowlist, and a short delay before we adopt a brand-new release. I'd send updates on a fixed schedule so people aren't asking in ten channels.”

Red flag to avoid:

Just bumping the version and moving on, without checking what ran in CI or rotating secrets.

They may ask next:
  • How would you know whether any secrets were actually used by an attacker?
  • How do you balance delaying new package versions against getting security fixes quickly?
Say it in 60 seconds

Architecture 2 questions

Hard System design round Senior Practice question

18. Twelve teams ship into one large web app, and it keeps getting slower and less stable. As the senior engineer, what guardrails would you put in place, and what would you leave to teams?

What the interviewer is really testing:
Whether you can steer many teams with automated checks and clear ownership, not with meetings and heroics.
Answer frame:

Measure real users: field data for load time and responsiveness per route, owned by named teams.

Budgets in CI: bundle size and dependency checks per route that fail the build, with an exception process.

Shared foundations: one lint config, one version of core libraries, error monitoring with source maps.

Leave to teams: internal structure, libraries inside their area, how they hit their numbers.

Sample spoken answer:

“First I'd make it visible. We'd collect real-user measurements of load time and responsiveness per route and per release, and every route gets an owning team, so a slowdown lands on someone's dashboard instead of everyone's. Then I'd turn the numbers into guardrails in CI: a size budget for each route's JavaScript and a check on new dependencies, both failing the build, with a quick, written way to ask for an exception. I'd standardise the things that hurt everyone when they differ: one shared lint config, one version of core libraries in the bundle, one error-monitoring setup with private source maps, and a supported-browsers list. What I'd leave to teams is how they structure their own area and how they meet their numbers. On my last platform, adding budgets turned slow creep into small, visible arguments during review, which is exactly where you want them.”

Red flag to avoid:

Relying on a performance review meeting or one expert fixing things by hand after every release.

They may ask next:
  • A team's launch breaks the budget and the business wants it live tomorrow. What do you do?
  • How would you split the app so one team's bug can't take down another team's pages?
Say it in 60 seconds
Hard System design round Senior Practice question

19. Your company's main web app is built on an old front-end framework nobody wants to maintain. How would you plan moving it to a new one while teams keep shipping features?

What the interviewer is really testing:
Whether you can plan an incremental migration with a clear boundary, a way to measure progress, and an end date.
Answer frame:

Seam: a shell that routes some pages to the new stack and the rest to the old one.

Order: new pages go new first, then high-change pages; leave quiet pages until last.

Bridge: a thin shared layer for auth, routing and state, owned by one team.

Finish line: track progress, set a date to remove the old framework, and budget the cost of running two.

Sample spoken answer:

“I'd never do a big-bang rewrite with features frozen. I'd find a seam, usually the route: a small shell decides which pages the new framework renders and which the old one does, so both can run side by side. Every new page is built on the new stack from day one, then we move the pages that change most often, because that's where the old code hurts most, and the quiet pages go last. One team owns a thin bridge for things both sides need, like the logged-in user, navigation and shared data, so teams don't invent their own glue. We'd track progress as a simple number of routes moved, and agree a date to delete the old framework, because two runtimes in one bundle cost load time and attention. Without that end date, migrations tend to stall with the hardest pages still on the old stack for years.”

Red flag to avoid:

Proposing a full rewrite with a feature freeze, or a migration with no way to measure progress or finish.

They may ask next:
  • How would you share state between the old and new parts without coupling them tightly?
  • What would make you stop the migration halfway and keep both?
Say it in 60 seconds

Leadership 4 questions

Medium Behavioral round Senior Practice question

20. Tell me about a time you pushed back on leadership about a big front-end decision, like a full rewrite or a framework switch. How did you make your case?

What the interviewer is really testing:
Whether you can turn a technical opinion into a business case, offer a real alternative, and accept the outcome.
Answer frame:

Situation: what was proposed, by whom, and why it mattered.

The case: risks and costs in their terms, like time with no new features, and a concrete alternative.

Evidence: a small experiment or data instead of opinion.

Outcome: what was decided, what happened, and what you'd do differently.

Sample spoken answer:

“At my last company, leadership wanted to rewrite our customer app from scratch because a competitor looked more modern. I agreed the app needed work, but a full rewrite meant most of a year with almost no new features. So instead of arguing in the meeting, I asked for two weeks. We rebuilt the two slowest pages on the new stack inside the existing app and measured load time and conversion against the old ones. The pages were clearly faster and we lost nothing. I showed the numbers next to a plan that moved the app over page by page, with features shipping the whole time. They went with the incremental plan. What I learned is that 'no' rarely works on its own; 'here's a cheaper way to get what you want, and here's proof' usually does. Next time I'd bring product in earlier.”

Red flag to avoid:

A story where you were simply right and leadership was simply wrong, with no alternative offered and no cost admitted.

They may ask next:
  • What would you have done if they'd insisted on the full rewrite anyway?
  • How did you keep the team motivated while the old and new code lived side by side?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

21. Tell me about a large JavaScript initiative you led that failed or had to be rolled back. What did you get wrong?

What the interviewer is really testing:
Whether you own failure honestly, can say what you misjudged, and changed how you work because of it.
Answer frame:

What you led: the goal, the scale and your role.

What went wrong: the real misjudgement, said plainly.

Recovery: how you stopped the damage and told people.

What changed: a concrete change in how you plan or roll out now.

Sample spoken answer:

“I led a move to a new shared state library across our main web app. I'd built a good prototype, the team liked it, and we rolled it out broadly within a quarter. What I got wrong was that I tested it on our simplest screens. The complicated screens had lots of real-time updates, and there the new library re-rendered far more than the old code, so the app got noticeably slower on the screens our biggest customers used most. We rolled back those screens, which cost a painful month, and I wrote the review myself, starting with my own decisions. Two things changed in how I work. I now pilot anything cross-cutting on the hardest part of the system first, not the easiest. And every rollout has agreed performance numbers checked before each wider step, so we stop early instead of finding out from customers.”

Red flag to avoid:

A failure that was really someone else's fault, or one so small it teaches nothing.

They may ask next:
  • How did you rebuild the team's trust in shared changes after the rollback?
  • Looking back, what early signal did you miss?
Say it in 60 seconds
Medium Behavioral round Senior Practice question

22. Tell me about a JavaScript engineer you helped grow from someone who delivers tickets into someone who could lead a part of the front end. What did you actually do?

What the interviewer is really testing:
Whether you grow people on purpose, with stretch work and real feedback, not just by being available for questions.
Answer frame:

Starting point: where they were strong and what held them back.

Stretch: real ownership with a safety net, not a side project.

Feedback: specific, regular, and about how they worked, not only the code.

Result: what they could do afterwards, and what you learned about mentoring.

Sample spoken answer:

“One engineer on my team wrote very clean components but waited to be told what to build and avoided anything outside her tickets. I gave her ownership of our checkout flow's performance, a real problem with a real deadline. I set it up so she wrote the plan and I only reviewed it, and we met every week for half an hour, mostly me asking questions. The first plan was too big, so we cut it together, and she presented the result to product herself. In code review I moved from commenting on details to asking about trade-offs, so she started explaining her choices before anyone asked. About a year later she led a small group through the checkout redesign. What I learned is that the stretch has to be something that matters, or people don't take ownership of it.”

Red flag to avoid:

Describing mentoring as answering questions when asked, with no deliberate plan or outcome.

They may ask next:
  • How did you handle it when her plan was going in a direction you disagreed with?
  • How do you decide who to invest this kind of time in?
Say it in 60 seconds
Medium Culture fit round Senior Practice question

23. You're asked to design the interview loop for senior JavaScript engineers. What would you test, and what separates a senior answer from a mid-level one?

What the interviewer is really testing:
Whether you know what senior means in practice and can hire for it fairly and consistently.
Answer frame:

Signals: debugging real problems, design trade-offs, reviewing code, and leading people.

Format: work-like exercises over trivia puzzles, the same questions and scoring for every candidate.

Senior answers: ask about constraints, name trade-offs and failure modes, admit what they don't know.

Fairness: trained interviewers, written notes before discussion, decisions against the rubric.

Sample spoken answer:

“I'd test what the job actually needs. One session debugging a small but real app, like a slow page or a flaky async bug, because that shows how someone thinks under uncertainty. One design session on a front-end problem with real constraints. One code review of a pull request with a few planted problems, since seniors spend a lot of time reviewing. And one conversation about leading work and disagreeing well. Every candidate gets the same exercises and a written rubric. What separates senior from mid-level isn't knowing more trivia. A senior asks about constraints before designing, names the trade-off they're choosing and what could fail, and says 'I'm not sure, here's how I'd find out' without getting nervous. I'd also have interviewers write their notes before the debrief, so the loudest voice doesn't decide.”

Red flag to avoid:

A loop built on puzzles and trivia, or deciding on gut feeling without a shared rubric.

They may ask next:
  • How would you check the loop isn't unfairly filtering out good candidates?
  • Would you use a take-home exercise? Why or why not?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card