JavaScript interview question
What is a closure? Show a practical use in test code.
Short answer
A closure is a function bundled with references to the variables of the scope where it was created, so it can still read and update them after that scope has returned. Closures power private state, factories, callbacks and memoisation, and explain the classic var-in-a-loop bug.
Answer
A closure is a function together with the variables from the scope where it was defined. When a function is created inside another function, it keeps a live reference to the outer function’s variables, so it can read and modify them even after the outer function has returned. Every JavaScript function forms a closure; it becomes interesting when the inner function outlives its parent.
A minimal example
function createCounter() {
let count = 0; // not reachable from outside
return {
next: () => ++count,
current: () => count,
};
}
const counter = createCounter();
counter.next(); // 1
counter.next(); // 2
counter.current(); // 2
// counter.count is undefined: the state is private
createCounter has returned, but count is still alive because the returned functions reference it. Each call to createCounter() creates a new, independent count.
Captured by reference, not by value
Closures capture variables, not snapshots of their values. That explains the classic loop bug:
const fns = [];
for (var i = 0; i < 3; i++) fns.push(() => i);
fns.map((f) => f()); // [3, 3, 3]: one shared i
const fns2 = [];
for (let j = 0; j < 3; j++) fns2.push(() => j);
fns2.map((f) => f()); // [0, 1, 2]: a fresh j per iteration
Practical uses in test code
Unique test data generators without global variables:
function uniqueEmailFactory(prefix = 'qa') {
const runId = Date.now().toString(36);
let n = 0;
return () => `${prefix}+${runId}-${++n}@example.com`;
}
const nextEmail = uniqueEmailFactory();
nextEmail(); // qa+lx3k9a-1@example.com
nextEmail(); // qa+lx3k9a-2@example.com
Memoising an expensive call, such as fetching an auth token once per worker:
function memoizeAsync(fn) {
let promise;
return () => (promise ??= fn());
}
const getToken = memoizeAsync(async () => {
const res = await fetch('https://api.example.com/auth', { method: 'POST' });
return (await res.json()).token;
});
Caching the promise (not the value) means concurrent callers share one in-flight request.
Configured helpers: const withBaseUrl = (base) => (path) => new URL(path, base).href; creates a function preconfigured for an environment. Playwright fixtures and many builder patterns rely on the same idea.
Pitfalls
- Unintended shared state: a module-level closure in a test helper is shared by every test in that worker, which can create order dependencies.
- Memory: a long-lived closure keeps everything it references alive. Rarely a problem in tests, but relevant for event listeners in apps.
What interviewers look for
- An accurate definition mentioning lexical scope and lifetime beyond the outer call.
- Capture by reference and the loop example.
- A real use, ideally from test code, not only the counter.
- Awareness of shared state between tests.
A strong answer covers
- Defines a closure as a function retaining access to its lexical scope
- Variables are captured by reference, not copied
- Shows private state or a factory function
- Explains the var-in-loop pitfall and the let fix
- Gives a practical testing use: data builders, unique IDs, memoised tokens