Q2 of 40 · JavaScript
Explain closures in JavaScript with a practical example.
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
RequestSpecclosure pre-populated with base URL and auth headers, returning a builder function that individual tests call. - Retry wrappers: a
withRetry(fn, attempts)that closes overfnandattempts. - Custom Playwright fixtures: the
usepattern intest.extendrelies on closures to hand control back to the test body. - Custom Cypress commands: the callback passed to
Cypress.Commands.addcloses 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