Quiz

What is a closure in JavaScript, and how/why would you use one?

Topics
ClosureJavaScript

TL;DR

In the book "You Don't Know JS" (YDKJS) by Kyle Simpson, a closure is defined as follows:

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope

In simple terms, functions have access to variables that were in their scope at the time of their creation. This is what we call the function's lexical scope. A closure is a function that retains access to these variables even after the outer function has finished executing. It is as if the function has a memory of its original environment.

function outerFunction() {
const outerVar = 'I am outside of innerFunction';
function innerFunction() {
console.log(outerVar); // `innerFunction` can still access `outerVar`.
}
return innerFunction;
}
const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.
inner(); // "I am outside of innerFunction"
// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.

Key points to remember:

  • Closure occurs when an inner function has access to variables in its outer (lexical) scope, even when the outer function has finished executing.
  • Closure allows a function to remember the environment in which it was created, even if that environment is no longer present.
  • Closures are used extensively in JavaScript, such as in callbacks, event handlers, and asynchronous functions.

Understanding JavaScript closures

In JavaScript, a closure is a function that captures the lexical scope in which it was declared, allowing it to access and manipulate variables from an outer scope even after that scope has been closed.

Here's how closures work:

  1. Lexical scoping: JavaScript uses lexical scoping, meaning a function's access to variables is determined by its actual location within the source code.
  2. Function creation: When a function is created, it keeps a reference to its lexical scope. This scope contains all the local variables that were in-scope at the time the closure was created.
  3. Maintaining state: Closures are often used to maintain state in a secure way because the variables captured by the closure are not accessible outside the function.

ES6 syntax and closures

With ES6, closures can be created using arrow functions, which provide a more concise syntax and lexically bind the this value. Here's an example:

const createCounter = () => {
let count = 0;
return () => {
count += 1;
return count;
};
};
const counter = createCounter();
console.log(counter()); // Outputs: 1
console.log(counter()); // Outputs: 2

Closures compared with classes

Closures and classes can both encapsulate state and expose operations on it. The two approaches differ in privacy mechanism, memory characteristics, and idiomatic fit.

// Closure-based implementation
function makeCounter() {
let count = 0;
return {
inc: () => ++count,
get: () => count,
};
}
// Class-based implementation with private fields
class Counter {
#count = 0;
inc() {
return ++this.#count;
}
get() {
return this.#count;
}
}
const a = makeCounter();
const b = new Counter();
console.log(a.inc(), a.inc()); // 1 2
console.log(b.inc(), b.inc()); // 1 2
ConcernClosureClass with #private fields
PrivacyLexical scope; inaccessible from outside the closurePrivate slot; access outside the class throws TypeError
Memory per instanceNew closure scope and new function objects per callInstance state per new; methods shared via the prototype
this bindingNot required; methods close over outer variablesMethods use this; additional care required for callbacks
Prototype sharingNot supported; each instance has its own methodsSupported; instance methods share the prototype
Typical useFactories, event handlers, partial application, FPLong-lived domain objects, inheritance hierarchies

General guidance:

  • A closure is appropriate when a small number of instances with encapsulated state are needed and inheritance is not a concern.
  • A class is appropriate when many instances share the same behavior (prototype sharing avoids duplicating methods per instance), when instanceof checks or inheritance are needed, or when the API is consumed through type annotations in TypeScript.

Closures in React

Closures are everywhere. The code below shows a simple example of increasing a counter on a button click. In this code, handleClick forms a closure. It has access to its outer scope variables count and setCount.

import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// `handleClick` is a closure over `count` and `setCount`.
function handleClick() {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

Stale closures in useEffect

A closure inside a useEffect callback captures the values of the variables it references at the time the effect runs. When those values change on subsequent renders, the closure continues to reference the originally captured values unless the effect re-runs or a different mechanism is used to read live state. This is a common cause of hooks-related bugs.

function Chat() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// `count` here refers to the value captured when the effect ran.
// With an empty dependency array, the effect runs only once, so this
// value is always 0.
console.log('current count:', count);
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}

There are three ways to correct this:

Declare the dependency. The effect re-runs whenever count changes, and a new closure captures the current value:

useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]);

This is correct, but creates a new interval every second.

Use the functional updater form of setState. The updater receives the current state as an argument, so no value needs to be captured:

useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

Use a ref. Useful when the callback should read live state but not re-run when the state changes:

const countRef = useRef(0);
useEffect(() => {
countRef.current = count;
});
useEffect(() => {
const interval = setInterval(() => {
console.log('current count:', countRef.current);
}, 1000);
return () => clearInterval(interval);
}, []);

The functional updater is usually the simplest correct option when the callback only needs to update state. The ref approach is appropriate when live state must be read without re-running the subscription.

Memoization with closures

Function memoization caches results of a computation against the arguments used to produce them. A closure holds the cache, keeping it scoped to the memoized wrapper.

function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const slowSquare = (n) => {
console.log('computing', n);
return n * n;
};
const fastSquare = memoize(slowSquare);
console.log(fastSquare(4)); // 'computing 4' then 16
console.log(fastSquare(4)); // 16 (cache hit; no 'computing' log)
console.log(fastSquare(5)); // 'computing 5' then 25

Observations:

  1. The cache variable is accessible only through the returned function. This is the "private state" property of closures applied to a practical utility.
  2. The cache grows unbounded by default, which can cause memory issues on long-running inputs. Production memoization implementations typically use an LRU cache or a WeakMap keyed on object identity. Further discussion of this and related issues is on the closure pitfalls page.
  3. The same pattern appears in widely used utilities, including React's useMemo and useCallback, Reselect's createSelector, and Lodash's _.memoize.

Common example: var in a loop

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3, 3, 3

var i is function-scoped, so all three callbacks close over the same binding. The loop completes and sets i to 3 before any setTimeout callback runs, because setTimeout(fn, 0) still queues the callback as a macrotask that runs after the current synchronous code.

Two corrections:

  • Replace var with let. let is block-scoped, so each iteration creates a fresh binding, and each callback closes over a different value.
  • Use an IIFE to create a new function scope per iteration: (i => setTimeout(() => console.log(i), 0))(i). This is the pre-ES6 alternative.

Common questions

When is a closure preferable to a class?

A closure is generally preferable when the expected number of instances is small, when inheritance and instanceof are not needed, and when avoiding this is desirable. A class is preferable when many instances will share the same behavior (prototype sharing), when inheritance is used, or when the API is consumed through TypeScript type annotations.

Can closures cause memory leaks?

Yes. A closure that references a large object and is itself reachable from long-lived state (module scope, event listener registrations, or a Redux store, for example) will keep the referenced object alive. The closure pitfalls page covers specific patterns and how to detect them.

Are closures synchronous or asynchronous?

A closure is simply a function that captures its lexical scope. The function may be invoked synchronously or asynchronously; the closure mechanism itself is independent of the invocation timing. The var-in-loop example above is a common source of confusion because the variable referenced by the closure changes between the closure's creation and its asynchronous invocation.

Why use closures?

Using closures provides the following benefits:

  1. Data encapsulation: Closures provide a way to create private variables and functions that can't be accessed from outside the closure. This is useful for hiding implementation details and maintaining state in an encapsulated way.
  2. Functional programming: Closures are fundamental in functional programming paradigms, where they are used to create functions that can be passed around and invoked later, retaining access to the scope in which they were created, e.g. partial applications or currying.
  3. Event handlers and callbacks: In JavaScript, closures are often used in event handlers and callbacks to maintain state or access variables that were in scope when the handler or callback was defined.
  4. Module patterns: Closures enable the module pattern in JavaScript, allowing the creation of modules with private and public parts.

Related questions

You can read more about memory leaks and other pitfalls of closures and the module pattern and private state on their dedicated pages.

Further reading

Edit on GitHub