Skip to content
SDETRole
SDETRole

JavaScript interview question

What is a closure? Show a practical use in test code.

intermediate 2–5 yrs #closures#scope#functions#factories

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
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →