JavaScript (ES2026) Module 4
Functions, scope and closures interview questions
Interview questions on functions, scope and closures: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What is the difference between a function declaration and a function expression?
Answered in the lesson Function declarations, expressions and return values
-
Fresher
When does a default parameter value apply in JavaScript?
Answered in the lesson Parameters, defaults and rest parameters
-
Fresher
What is the difference between an arrow function and a regular function?
-
Fresher
What does lexical scope mean in JavaScript?
Answered in the lesson Scope, the scope chain and execution contexts
-
Fresher
What is hoisting in JavaScript, and are let and const hoisted?
Answered in the lesson Hoisting, var and the temporal dead zone
-
Fresher
What is a closure in JavaScript?
-
Fresher
What is a higher-order function? Give two examples from JavaScript's standard library.
-
Fresher
What does the base case of a recursive function do, and what goes wrong without one?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to mid
After an automatic reformat, getRetryPolicy() in a shared module returns undefined, and every caller silently falls back to its own hard-coded retry values. Its return statement now has the object literal on the next line. Explain what the parser does, why nothing failed loudly, and how you stop it from happening again.
- What it tests
- Automatic semicolon insertion and restricted productions, reading undefined as a symptom, and preventing the bug with a linter and tests of return values.
- First step
- Read the function the way the parser does. return may not be followed by a line break, so the statement ends there and the function returns undefined. The object literal below it is parsed as a block with a label, which is valid code that never runs.
- Follow-ups
-
- Why did the module not fail with a SyntaxError?
- Which tools would have caught it before it shipped?
- Where else does a line break change what a statement means?
From the lesson Function declarations, expressions and return values
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
Your error tracker groups crashes by their top stack frames, and most of the biggest groups are titled "(anonymous)". Which functions get a name automatically in JavaScript, which do not, and what would you change in the code base so that new crashes group under readable names?
- What it tests
- Function name inference, named function expressions, how names reach stack traces, and conventions and build settings that keep them.
- First step
- Find where the anonymous frames come from. Functions written directly as arguments, such as callbacks to map, then or addEventListener, get no name, while functions assigned to a variable, a property in an object literal or a default parameter take that name through name inference.
- Follow-ups
-
- Does an arrow function stored in a const get a name?
- What does a bound function's name look like, and which frame does a trace show?
- The production bundle is minified. What else do you need?
From the lesson Function declarations, expressions and return values
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A teammate speeds up a lookup with function priceOf(sku, cache = new Map()), which checks the cache first and stores every price it fetches. In production the database still sees every single lookup. Explain why the cache never hits, where it should live instead, and how a test would have caught it.
- What it tests
- Knowing that defaults are evaluated on every call, choosing where long-lived state belongs, and testing caching with a fake data source.
- First step
- Ask when the default runs: on every call that leaves cache out. Each call builds a new, empty Map, stores one price in it and drops it when the call returns, so the next call checks a different, empty Map and goes to the database again.
- Follow-ups
-
- Why would the same idea work in Python, and what goes wrong there instead?
- Module scope, a closure or a class field: where would you keep the cache, and what does each cost?
- The cache now works, but memory grows all day. What do you change?
From the lesson Parameters, defaults and rest parameters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A dashboard computes Math.max(...readings) over a day of sensor readings. It worked for months, then a busy day crashed it with "RangeError: Maximum call stack size exceeded", although nothing recursive runs. Explain the mechanism, roughly where the limit is, and how you fix it and keep it fixed.
- What it tests
- Knowing that spread turns each element into an argument, that engines cap a call's arguments, and choosing code that works for any size.
- First step
- Spread passes every element as a separate argument, so a day of 150,000 readings becomes one call with 150,000 arguments. V8 places a call's arguments on its call stack, and that many do not fit, which is why the error names the stack although nothing recurses.
- Follow-ups
-
- Is Math.max.apply(null, readings) a fix?
- Why not raise the stack size with node --stack-size?
- Where else in a code base does the same limit hide?
From the lesson Parameters, defaults and rest parameters
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Fresher to mid
A Ticker class calls setInterval(this.tick, 1000) in start(), and tick() runs this.count += 1. In Node.js the counter stays at 0 without any error; under fake timers that pass no this, tick() throws a TypeError. Explain both, then compare three fixes: an arrow class field, bind in the constructor, and an arrow at the call site.
- What it tests
- How a method loses its this when passed as a callback, what different hosts pass as this, and the costs of the usual fixes.
- First step
- this.tick passes the function alone, without the object, and setInterval calls it later as a plain callback. Node.js passes its Timeout object as this, so the addition lands on the timer, not on the ticker; a fake timer passes undefined, and reading count from undefined throws.
- Follow-ups
-
- Why does the ticker stay at 0 instead of showing NaN?
- Which fix still lets a subclass override tick()?
- The same handler is also added with addEventListener. Which fixes let you remove it again?
From the lesson Arrow functions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A pull request converts every function in a 2,000-line module to an arrow function "for consistency". The tests pass. As the reviewer, what can break, what gets worse even when nothing breaks, and what would you accept?
- What it tests
- Knowing everything arrows lack (this, arguments, prototype, new, hoisting, generators), judging risk beyond the tests, and setting a team convention.
- First step
- Search the diff for the features arrows do not have: this inside converted methods, arguments, functions called with new or used as prototypes, generator functions, and calls that come before the definition. Each one is a behaviour change, whatever the tests say.
- Follow-ups
-
- Why can a converted method pass every test and still be wrong?
- What does the conversion do to calls above a function's definition?
- What convention would you write down for the team?
From the lesson Arrow functions
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A page loads analytics.js and then app.js as classic scripts. After analytics.js is updated, app.js stops with "SyntaxError: Identifier 'config' has already been declared", and none of its lines run. Explain why, why var in both files would have hidden the problem, and how you fix it for good.
- What it tests
- The shared global scope of classic scripts, the global object against the global lexical declarations, the check before a script runs, and isolating code with modules.
- First step
- All classic scripts of a page share one global scope. The update added a top-level let or const config to analytics.js; app.js declares config at its top level too, and before running a script the engine checks its top-level declarations against the names that already exist, so app.js is refused whole.
- Follow-ups
-
- What would var config in both files have done instead?
- Why does the whole of app.js fail, not just the line with config?
- How do you isolate code you do not control, such as a third-party snippet?
From the lesson Scope, the scope chain and execution contexts
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
In a classic browser script, var name = { first: 'Asha' }; console.log(name.first); prints undefined. The same two lines in a module, or in Node.js, print Asha. Explain the difference, and what it tells you about top-level declarations in classic scripts.
- What it tests
- How top-level var binds to properties of the global object, accessors on window, and why modules and let avoid the problem.
- First step
- Ask where the variable lives. A top-level var in a classic script is a property of the global object, and window already has a name property, an accessor that stores the window's name as a string. The declaration reuses that property, so the assignment calls its setter and stores "[object Object]".
- Follow-ups
-
- Why does let name = { first: 'Asha' } at the top of the same script print Asha?
- Which other window properties behave like this?
- How would you find such clashes in an old code base?
From the lesson Scope, the scope chain and execution contexts
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate replaced every var with let in a 600-line legacy checkout script. All tests pass, but in production a rarely used path now throws a ReferenceError. How do you find every place like it before the next release, and what do you put in place so that it cannot happen again?
- What it tests
- What var allowed that let does not, telling the two kinds of ReferenceError apart, finding them with lint rules and coverage, and converting safely.
- First step
- Ask what var allowed that let does not: a name used outside the block that declares it, or read above its declaration. Both now throw, but only on the path that reaches them, which is why the tests stayed green. The error message tells you which kind you have; then let the linter list the others instead of reading 600 lines.
- Follow-ups
-
- Why did the tests not catch it?
- Could swapping var for let change behaviour without throwing?
- Would you convert the file by hand or with a tool?
From the lesson Hoisting, var and the temporal dead zone
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
config.mjs imports defaults from rules.mjs, and rules.mjs imports region from config.mjs to build those defaults. Starting the app with config.mjs fails with "ReferenceError: Cannot access 'region' before initialization". Explain the order in which the two modules run, why an exported function would have survived, and how you fix it.
- What it tests
- How ES modules are linked and then run, import cycles, the temporal dead zone across modules, and restructuring shared values.
- First step
- Work out which module runs first. The entry, config.mjs, imports rules.mjs, so rules.mjs runs before it, even though rules.mjs imports config.mjs too: that module is already in progress. rules.mjs reads region at its top level while the const line of config.mjs has not run, so region is still in its temporal dead zone.
- Follow-ups
-
- Why does the error move to the other module when the entry point changes?
- Why do function declarations work here?
- How do you find cycles in a large code base?
From the lesson Hoisting, var and the temporal dead zone
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A dashboard's memory grows by about 40 MB an hour. After a working day, a heap snapshot shows over a hundred 2 MB report arrays, kept alive through the contexts of small click handlers that only read a report's title. Explain how the closures keep them alive, how you would fix it and how you would prove the fix.
- What it tests
- What a closure keeps alive, registered listeners as roots, contexts shared by the closures of one call, removing listeners and capturing less, and proving a leak fix.
- First step
- Ask what holds the handlers. A registered listener stays alive until it is removed, and each handler keeps the environment of the call that made it. Check whether the panel adds new listeners on every render without removing the old ones, and which variables of that render call its handlers share.
- Follow-ups
-
- Why does a handler that never mentions the report keep it alive?
- Would a WeakRef or a WeakMap fix it?
- How do you stop it from coming back?
From the lesson Closures in JavaScript
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Write once() for an SDK's asynchronous init(), which 20 parts of a page await during start-up. What should a caller get while the first call is still running, and after it has failed? Walk through the edge cases you would decide on and test.
- What it tests
- Caching a promise rather than a result, concurrent callers, retrying after a failure, synchronous throws, this and arguments, and testing with a controllable fake.
- First step
- Decide what is remembered. Caching the result only once the work has finished lets every caller that arrives in between start its own request, so cache the promise itself: the first call creates it, and every later call returns that same promise, settled or not.
- Follow-ups
-
- What should a second caller get while the first call is still running?
- Should a failure be cached?
- How would you test it?
From the lesson Closures in JavaScript
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
An import runs 50,000 rows through pipe(parseRow, validate, enrich, save). One malformed row throws inside enrich, and the import stops after 12,000 rows with nothing reported for the rest. How would you change the pipeline so that bad rows are reported and skipped, and what do you trade off?
- What it tests
- Error handling in function pipelines, wrapping steps with higher-order helpers, reporting rejected rows, failure thresholds and saves that are safe to repeat.
- First step
- Find where the error escapes. A step that throws ends the pipeline's call, and nothing catches it per row, so the loop stops with it. Decide what a failed row should mean, then catch errors around each row's pipeline, record the row number and the step, and go on with the next row.
- Follow-ups
-
- What changes if enrich is asynchronous?
- Why not let enrich return null for bad rows?
- How do you test it?
From the lesson Higher-order functions and callbacks
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
getRate(code, callback) calls back at once when the rate is cached and after a network request otherwise. Checkout code that sets a variable after calling getRate() works in every test, but in production most requests show a wrong total or a ReferenceError. Explain why, and how you would redesign getRate().
- What it tests
- When callbacks run, functions that call back sometimes at once and sometimes later, why tests miss it, and deferring callbacks or returning promises.
- First step
- Compare the two paths. On a cache miss the callback runs after the checkout code has finished; on a hit it runs inside getRate(), before the caller's next line has run. Tests start with an empty cache, so they only ever take the miss path.
- Follow-ups
-
- Why did the tests not catch it?
- Is calling back synchronously ever right?
- Does queueMicrotask() or setTimeout(…, 0) make a difference here?
From the lesson Higher-order functions and callbacks
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service validates partner JSON with a recursive walk. A partner starts sending a 2 MB document nested 30,000 levels deep, and every request now fails with RangeError: Maximum call stack size exceeded. What do you change, and how do you protect the service from such input in general?
- What it tests
- Call stack limits, which parts of the platform recurse, rewriting a walk with an explicit stack, and depth and size limits on input.
- First step
- Find the code that overflows. Each level of the document costs at least one frame in a recursive walk, and an engine allows only a few thousand, so 30,000 levels cannot work. Check the service's own walk first, then recursive helpers that run on the same document, such as JSON.stringify in the error log.
- Follow-ups
-
- Why does every request fail, not just one?
- Would raising Node's stack size fix it?
- How do you find the other recursive code paths?
From the lesson Recursion and the call stack
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A nightly job walks 2 million folders recursively, and the deepest path has 40 levels. A teammate wants to rewrite it with an explicit stack so that it can never crash. Is the rewrite worth it? Say what you would check first, and when you would insist on it.
- What it tests
- Depth against size, the measured stack limit, cycles from links, the memory an explicit stack needs, and when recursion is the better code.
- First step
- Separate size from depth. Two million folders means two million calls in all, but only the calls on the current path wait on the stack at the same time, about 40 here. Compare that with the few thousand frames a JavaScript engine allows.
- Follow-ups
-
- What would you check before deciding?
- Does an explicit stack use less memory?
- When is recursion the better code?
From the lesson Recursion and the call stack
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
All JavaScript (ES2026) interview questions · The JavaScript (ES2026) track