Q2 of 40 · JavaScript

Explain closures in JavaScript with a practical example.

JavaScriptMidjavascript-closuresscopelexical-scopecallbacks

Short answer

Short answer: A closure is a function bundled with live references to the variables in its surrounding lexical scope. Even after the outer function returns, the inner function retains access to those variables — not copies, but the actual bindings. In test automation, closures appear in fixture factories, retry wrappers, and custom command builders.

Detail

When a function is created in JavaScript, it captures a reference to its enclosing scope's variable environment. That bundle — function + scope reference — is a closure. The inner function retains live access to the outer variables, not a snapshot of their values at creation time.

This is why the classic for loop + setTimeout bug bites people. With var, all callbacks share the same i binding; by the time the timeouts fire the loop has finished and i is 3. With let, each iteration creates a new binding, so each callback closes over its own i.

In test automation, closures appear constantly:

  • Fixture factories: a helper that creates a base RequestSpec closure pre-populated with base URL and auth headers, returning a builder function that individual tests call.
  • Retry wrappers: a withRetry(fn, attempts) that closes over fn and attempts.
  • Custom Playwright fixtures: the use pattern in test.extend relies on closures to hand control back to the test body.
  • Custom Cypress commands: the callback passed to Cypress.Commands.add closes over test config.

Understanding closures helps you read and debug the callback-heavy code that test frameworks produce, and write helper utilities that are cleaner than their class-based equivalents.

// EXAMPLE

closure-api-client.js

// Closure for a reusable API client in test helpers
function createApiClient(baseUrl, authToken) {
  // baseUrl and authToken are captured in the closure
  return {
    async get(path) {
      const res = await fetch(`${baseUrl}${path}`, {
        headers: { Authorization: `Bearer ${authToken}` },
      });
      return res.json();
    },
    async post(path, body) {
      const res = await fetch(`${baseUrl}${path}`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${authToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
      });
      return res.json();
    },
  };
}

// Each test suite gets its own client with different base URLs
const prodClient = createApiClient("https://api.prod.com", process.env.PROD_TOKEN);
const stagingClient = createApiClient("https://api.staging.com", process.env.STAGING_TOKEN);

const user = await prodClient.get("/users/1");
// baseUrl and authToken are "remembered" by the closure

// WHAT INTERVIEWERS LOOK FOR

Correct definition (function + lexical scope reference, not copies), awareness of the var/let loop gotcha to demonstrate live vs snapshot distinction, and at least one concrete test automation use case. Candidates who can only recite the definition without applying it rarely use closures fluently.

// COMMON PITFALL

Conflating closures with callbacks. All closures can be callbacks, but a plain callback that doesn't reference outer variables is technically not a closure. The closure is specifically the mechanism of capturing the surrounding scope.