These 27 JavaScript interview questions test the language itself: scope, closures, prototypes, coercion, the event loop, async control flow, modules and memory. Framework and browser questions live on our React and front-end pages, and type-system questions on the TypeScript page. At Ryz we assess senior JavaScript engineers through recruiter sourcing, structured NTRVSTA AI interviews that probe how candidates reason about code, and recruiter review before and after. The AI scores are advisory, and people make every decision.
JavaScript has a lot of trivia that is easy to memorize and rarely matters. Use these questions to find out whether a candidate can predict what code does and explain why, because that is what debugging production issues requires. Show a snippet, ask what it logs, then ask how they would change it.
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
The first loop logs 3, 3, 3. var is function-scoped, so all three callbacks close over one binding that equals 3 when they run. The second logs 0, 1, 2 because let creates a fresh binding per iteration.
What a strong answer shows: They explain per-iteration bindings rather than saying "let fixes it", and connect it to closures capturing variables, not values.
A closure is a function together with the lexical environment it was created in, so it can read and update variables from outer scopes after those scopes return. Practical uses: private state in factories, memoization, partial application, and event handlers that remember configuration.
What a strong answer shows: A real example, plus the cost: closures keep their captured scope alive, which can retain large objects longer than intended.
let, const and class declarations are hoisted to the top of their block, but they are uninitialized until the declaration runs. Accessing them earlier throws a ReferenceError. var is hoisted and initialized to undefined, and function declarations are hoisted with their body.
What a strong answer shows: They can say why TDZ errors show up with circular module imports, where a binding is read before its module finishes evaluating.
this determined?For regular functions, by the call site: method call uses the object before the dot, plain calls get undefined in strict mode, new binds the new object, and call, apply and bind set it explicitly. Arrow functions have no this of their own and use the enclosing scope's.
What a strong answer shows: They explain why passing obj.method as a callback loses this, and when an arrow function is the wrong choice, such as an object method that needs dynamic this.
[] + {}, "5" * "2" and null == 0.[] + {} converts both to primitives: "" plus "[object Object]". "5" * "2" is 10, because * converts to numbers. null == 0 is false: loose equality treats null as equal only to undefined, even though null >= 0 is true because relational operators convert to numbers.
What a strong answer shows: They know + prefers strings when either side becomes one, use === by default, and accept x == null as an idiom for null or undefined.
null, undefined and an undeclared variable?undefined is the value of declared but unassigned bindings, missing properties and missing arguments. null is an explicit "no value". Reading an undeclared variable throws a ReferenceError, except under typeof. ?? and ?. treat both null and undefined as nullish, unlike ||, which also skips 0 and empty strings.
What a strong answer shows: They pick ?? over || for defaults where 0 or "" are valid values.
structuredClone handle that spread does not?Spread and Object.assign make shallow copies. JSON.parse(JSON.stringify(x)) loses dates, undefined, Maps, Sets and cycles. structuredClone deep-copies Maps, Sets, dates, typed arrays and circular references, but not functions, DOM nodes or class prototypes.
What a strong answer shows: They ask whether a deep copy is needed at all, and know cloned class instances come back as plain objects.
class declarations add?Each object has an internal [[Prototype]] link. Property lookup walks that chain until it finds the key or reaches null. class is mostly syntax over constructor functions and prototypes, but adds real semantics: TDZ, strict mode, non-enumerable methods, required new, super, and private #fields that are not on the prototype at all.
What a strong answer shows: They can draw the chain for an instance, its class's prototype and Object.prototype, and know #private is enforced by the engine, unlike underscore conventions.
Map, Set, WeakMap and WeakRef instead of plain objects?Map for keys of any type, frequent additions and removals, and guaranteed insertion order. Set for uniqueness. WeakMap for metadata attached to objects you do not own, without preventing garbage collection. WeakRef and FinalizationRegistry are rarely right: GC timing is not guaranteed.
What a strong answer shows: They avoid using objects as dictionaries with untrusted keys because of prototype keys like __proto__.
ES modules are statically analyzable: imports are resolved before code runs, exports are live bindings, and top-level await is allowed. CommonJS require is synchronous and runtime-evaluated, and exports are a copied object. That static structure enables tree shaking. Mixing them causes default-export interop issues.
What a strong answer shows: They understand live bindings and know dynamic import() is the way to load code conditionally in ESM.
async function loadAll(ids) {
const results = [];
ids.forEach(async (id) => {
results.push(await fetchUser(id));
});
return results;
}
forEach ignores the returned promises, so the function returns an empty array before any fetch finishes, and rejections become unhandled. Use Promise.all(ids.map(fetchUser)) for parallel loading or a for...of loop for sequential.
What a strong answer shows: They ask how many IDs there are and propose a concurrency limit rather than firing 10,000 requests at once.
Promise.all, allSettled, race and any.all resolves with every value or rejects on the first rejection. allSettled waits for all and reports each outcome. race settles with the first to settle. any resolves with the first fulfillment and rejects with an AggregateError only if all reject.
What a strong answer shows: They note that Promise.all does not cancel the remaining work, which is why they pass an AbortSignal.
Promises cannot be cancelled, so pass an AbortSignal through. fetch accepts one natively, AbortSignal.timeout(ms) gives deadlines, and AbortSignal.any([...]) combines user cancel with timeouts.
async function search(q, { signal }) {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]),
});
if (!res.ok) throw new Error(`search failed: ${res.status}`);
return res.json();
}
What a strong answer shows: They handle AbortError separately from real failures so a cancelled request does not show an error to the user.
Any object with [Symbol.iterator] works with for...of, spread and destructuring. Generators make writing them easy and lazy. Async generators with for await...of fit paginated APIs and streams. Iterator helpers such as .map, .filter and .take on iterators are now in modern engines.
What a strong answer shows: A real case, such as consuming a paginated API lazily, and awareness that each element should be awaited before the next page is requested.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
queueMicrotask(() => console.log("D"));
(async () => { console.log("E"); await null; console.log("F"); })();
console.log("G");
A, E, G, C, D, F, B. Synchronous code runs first, including the async function up to its first await. Then the microtask queue drains in order: C, D, then the continuation F. The timer callback is a macrotask and runs after microtasks are empty.
What a strong answer shows: They describe the loop as "one task, then all microtasks, then possibly render", rather than memorizing the answer.
Yes. The loop drains the entire microtask queue before moving on, so a promise chain that keeps scheduling more microtasks blocks timers, I/O and rendering just as a while loop would. Breaking work into macrotasks with setTimeout or scheduler.yield() where supported lets other work run.
What a strong answer shows: They understand async does not mean non-blocking for CPU work.
Find promises created without a rejection handler: fire-and-forget calls, missing await, or .then without .catch. Async stack traces in modern engines help. Fix by awaiting, by attaching handlers at creation, and by enabling lint rules such as no-floating-promises and no-misused-promises.
What a strong answer shows: They treat the global handler as a last-resort logger, not a fix.
async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
Each worker takes the next index synchronously, so no two workers share one, and results keep input order. A failure rejects the whole call while the other workers keep going.
What a strong answer shows: They explain why next++ is safe in single-threaded JavaScript, and discuss whether to stop on first error.
Debounce waits until calls stop for N ms, good for search-as-you-type. Throttle runs at most once per N ms, good for scroll and resize handlers. A debounce keeps a timer in a closure, clears it on each call and sets a new one.
What a strong answer shows: They discuss leading versus trailing calls, cancelling pending work on unmount, and preserving this and arguments.
Take heap snapshots in Chrome DevTools before and after repeating a navigation several times, compare retained objects, and follow retainer paths. Usual causes: event listeners and intervals not removed, detached DOM nodes held by closures or caches, growing module-level arrays or maps, and observers not disconnected.
What a strong answer shows: A repeatable method, and knowledge of AbortController signals as a clean way to remove many listeners at once.
Clear module boundaries with explicit public entry points, dependency direction enforced by lint rules, pure functions for logic, side effects pushed to edges, and JSDoc types checked with TypeScript's checkJs if migration to TypeScript is not planned.
What a strong answer shows: They focus on boundaries and testability, and can describe migrating incrementally rather than rewriting.
JavaScript numbers are IEEE 754 doubles, so 0.1 + 0.2 !== 0.3. Store money as integer minor units, or as decimal strings from the server, and format with Intl.NumberFormat. BigInt works for large integers but cannot mix with numbers.
What a strong answer shows: They fix the data model rather than calling toFixed at display time, and agree on rounding rules with the backend.
Store and transmit instants in UTC ISO 8601, keep the user's IANA time zone separately, and format with Intl.DateTimeFormat. The legacy Date API is error-prone with time zones and month arithmetic. Temporal fixes this and is shipping in engines, with a polyfill where it is not available.
What a strong answer shows: They separate instants from wall-clock times and know daylight saving bugs firsthand.
It happens when untrusted input like {"__proto__": {"isAdmin": true}} is deep-merged into an object, adding properties to Object.prototype. Defenses: validate input with a schema, use Object.create(null) or Map for dictionaries, reject __proto__, constructor and prototype keys in merge helpers, and keep dependencies patched.
What a strong answer shows: They know the vulnerable pattern (recursive merge, path setters) and how pollution escalates.
Weigh size and tree-shakability, maintenance activity, transitive dependencies, license and security history against the effort and risk of writing it. Small utilities that the platform now covers, such as structuredClone, Object.groupBy or Array.prototype.toSorted, rarely need a package.
What a strong answer shows: They consider supply-chain risk and lockfile hygiene, not only convenience.
Prefer real async code with fake timers only where time is the subject. Always await or return promises in tests, test rejection paths explicitly, and mock at the network boundary with tools such as MSW rather than deep inside modules.
What a strong answer shows: They explain why a test that forgets to await passes falsely, and keep flaky timing out of the suite.
When profiling shows it: tight loops over large arrays, repeated JSON parsing, regex backtracking, or accidental quadratic work such as array.includes inside a loop. Use the Performance panel or --cpu-prof to find hot functions, then fix algorithms before micro-optimizing.
What a strong answer shows: They reach for better data structures first and mention catastrophic regex backtracking as a denial-of-service risk.
setTimeout callback.async callbacks with forEach and expects them to be awaited.class as identical to classes in Java or C#.== everywhere, or bans it without understanding the == null idiom.Run a 75-minute pairing session with no framework. Provide a mock paginated API (a small module returning promises with random latency and occasional failures) and ask the candidate to build a function that fetches all pages with a concurrency limit of 3, retries failures with exponential backoff, supports cancellation with an AbortSignal, and returns results in page order. Give them a test file with a couple of starting cases and let them use any editor and documentation.
Ryz can supply engineers who already pass this bar. Hire senior JavaScript developers from the top 1% of candidates we interview, on your team and in your repos, working within ±1h of US time zones. Read how candidates are assessed on our vetting process page, or use our JavaScript developer job description as a starting point for your own posting.
var and coercion?A few, yes. Modern code avoids var and loose equality, but legacy code and libraries still use them, and the answers reveal whether a candidate understands scope and conversion rules or only follows lint rules.
A JavaScript interview tests runtime semantics that every framework sits on: closures, the event loop, prototypes and modules. React interviews test rendering and component design, and TypeScript interviews test the type system. Strong front-end and Node.js engineers need all of the JavaScript layer.
It works when the snippet is short and you ask for an explanation, not just the answer. It shows whether the candidate has an accurate mental model of the runtime, which is what debugging needs. Avoid puzzles that only reward memorized edge cases.
Questions we didn't answer? Email info@ryzlabs.com.