Practical Use Cases for JS Closures in Real Apps

Learn practical closure patterns used in production JavaScript. Covers debounce, throttle, currying, partial application, once functions, and state machines using closures.

JavaScriptintermediate
13 min read

Closures are more than a theory topic. Every debounce function, every throttle utility, every memoized computation, and every module in your codebase relies on closures. This guide covers real patterns you will use in production code, with full working examples.

Debounce: Delay Until Idle

Debounce delays a function call until the user stops triggering it. Each new trigger resets the timer. This is essential for search inputs, window resize handlers, and form validation:

javascriptjavascript
function debounce(fn, delay) {
  let timerId = null; // Private via closure
 
  return function (...args) {
    clearTimeout(timerId);
 
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
 
// Usage: Only fires after user stops typing for 300ms
const searchInput = document.querySelector("#search");
 
const handleSearch = debounce((event) => {
  console.log("Searching for:", event.target.value);
  // Fetch search results here
}, 300);
 
searchInput.addEventListener("input", handleSearch);

With Immediate Option

Some debounce implementations fire immediately on the first trigger and then wait for idle:

javascriptjavascript
function debounce(fn, delay, immediate = false) {
  let timerId = null;
 
  return function (...args) {
    const callNow = immediate && timerId === null;
 
    clearTimeout(timerId);
 
    timerId = setTimeout(() => {
      timerId = null;
      if (!immediate) fn.apply(this, args);
    }, delay);
 
    if (callNow) fn.apply(this, args);
  };
}

Throttle: Limit Execution Rate

Throttle ensures a function runs at most once within a given time window. Use it for scroll handlers, mouse move tracking, and API rate limiting:

javascriptjavascript
function throttle(fn, limit) {
  let waiting = false;
  let lastArgs = null;
 
  return function (...args) {
    if (waiting) {
      lastArgs = args; // Save the latest call
      return;
    }
 
    fn.apply(this, args); // Execute immediately
    waiting = true;
 
    setTimeout(() => {
      waiting = false;
      if (lastArgs) {
        fn.apply(this, lastArgs); // Run the trailing call
        lastArgs = null;
      }
    }, limit);
  };
}
 
// Usage: Fires at most once every 200ms during scroll
window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("Scroll position:", window.scrollY);
  }, 200)
);

Debounce vs Throttle Comparison

FeatureDebounceThrottle
Fires whenUser stops triggeringAt fixed intervals during triggering
Best forSearch input, form validation, resize endScroll tracking, mouse moves, rate limiting
Missed callsDiscarded (only the last matters)Saved as trailing call
First triggerDelayed (unless immediate: true)Immediate

Once: Run Exactly One Time

A once wrapper ensures a function only executes on the first call. Subsequent calls return the first result:

javascriptjavascript
function once(fn) {
  let called = false;
  let result;
 
  return function (...args) {
    if (called) return result;
 
    called = true;
    result = fn.apply(this, args);
    return result;
  };
}
 
const initialize = once(() => {
  console.log("App initialized");
  return { ready: true };
});
 
initialize(); // "App initialized" -> { ready: true }
initialize(); // Returns { ready: true } without logging
initialize(); // Same cached result

With Reset Capability

javascriptjavascript
function onceWithReset(fn) {
  let called = false;
  let result;
 
  function wrapper(...args) {
    if (called) return result;
    called = true;
    result = fn.apply(this, args);
    return result;
  }
 
  wrapper.reset = () => {
    called = false;
    result = undefined;
  };
 
  return wrapper;
}

Currying: One Argument at a Time

Currying transforms a function that takes multiple arguments into a chain of functions that each take one argument. The closure stores previously provided arguments:

javascriptjavascript
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
 
    // Return a new function that waits for more arguments
    return function (...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs]);
    };
  };
}
 
// A function that takes 3 arguments
function addThree(a, b, c) {
  return a + b + c;
}
 
const curriedAdd = curry(addThree);
 
// All these work:
console.log(curriedAdd(1, 2, 3));     // 6
console.log(curriedAdd(1)(2)(3));     // 6
console.log(curriedAdd(1, 2)(3));     // 6
console.log(curriedAdd(1)(2, 3));     // 6

Practical Currying Example

javascriptjavascript
const formatLog = curry((level, module, message) => {
  const timestamp = new Date().toISOString();
  return `[${timestamp}] [${level}] [${module}] ${message}`;
});
 
// Pre-configure loggers for different contexts
const errorLog = formatLog("ERROR");
const authError = errorLog("AUTH");
const dbError = errorLog("DB");
 
console.log(authError("Invalid token"));
// [2026-03-05T22:10:00.000Z] [ERROR] [AUTH] Invalid token
 
console.log(dbError("Connection timeout"));
// [2026-03-05T22:10:00.000Z] [ERROR] [DB] Connection timeout

Partial Application: Pre-Fill Some Arguments

Partial application fixes some arguments and returns a function that takes the rest. Unlike currying, it fills multiple arguments at once:

javascriptjavascript
function partial(fn, ...presetArgs) {
  return function (...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  };
}
 
function request(method, baseURL, endpoint, data) {
  console.log(`${method} ${baseURL}${endpoint}`, data || "");
  // In a real app: return fetch(...)
}
 
const apiRequest = partial(request, "GET", "https://api.example.com");
apiRequest("/users");        // GET https://api.example.com/users
apiRequest("/posts", { limit: 10 }); // GET https://api.example.com/posts { limit: 10 }
 
const postToAPI = partial(request, "POST", "https://api.example.com");
postToAPI("/users", { name: "Alice" }); // POST https://api.example.com/users { name: "Alice" }

Stateful Iterators

Closures allow you to create custom iterators that maintain their position:

javascriptjavascript
function createRangeIterator(start, end, step = 1) {
  let current = start;
 
  return {
    next() {
      if (current > end) {
        return { done: true, value: undefined };
      }
      const value = current;
      current += step;
      return { done: false, value };
    },
 
    reset() {
      current = start;
    },
 
    peek() {
      return current <= end ? current : undefined;
    }
  };
}
 
const iter = createRangeIterator(1, 5);
console.log(iter.next()); // { done: false, value: 1 }
console.log(iter.next()); // { done: false, value: 2 }
console.log(iter.peek()); // 3
console.log(iter.next()); // { done: false, value: 3 }

State Machines

Closures are perfect for simple state machines where the current state is private:

javascriptjavascript
function createTrafficLight() {
  const transitions = {
    green: "yellow",
    yellow: "red",
    red: "green"
  };
 
  const durations = {
    green: 30000,
    yellow: 5000,
    red: 20000
  };
 
  let current = "red";
  let timerId = null;
 
  return {
    getState() {
      return current;
    },
 
    next() {
      current = transitions[current];
      return current;
    },
 
    startAuto(onChange) {
      const tick = () => {
        this.next();
        if (onChange) onChange(current);
        timerId = setTimeout(tick, durations[current]);
      };
      timerId = setTimeout(tick, durations[current]);
    },
 
    stop() {
      clearTimeout(timerId);
      timerId = null;
    }
  };
}
 
const light = createTrafficLight();
console.log(light.getState()); // "red"
light.next();
console.log(light.getState()); // "green"

Middleware Chain

Build middleware pipelines (like Express.js) using closures:

javascriptjavascript
function createPipeline() {
  const middlewares = [];
 
  return {
    use(fn) {
      middlewares.push(fn);
      return this; // Enable chaining
    },
 
    execute(context) {
      let index = 0;
 
      function next() {
        if (index >= middlewares.length) return;
        const middleware = middlewares[index++];
        middleware(context, next);
      }
 
      next();
      return context;
    }
  };
}
 
const pipeline = createPipeline();
 
pipeline
  .use((ctx, next) => {
    ctx.timestamp = Date.now();
    next();
  })
  .use((ctx, next) => {
    ctx.user = ctx.user || "anonymous";
    next();
  })
  .use((ctx, next) => {
    console.log(`[${ctx.timestamp}] User: ${ctx.user} -> ${ctx.path}`);
    next();
  });
 
pipeline.execute({ path: "/home", user: "Alice" });

Event Emitter

A minimal pub/sub system using closures for private subscriber storage:

javascriptjavascript
function createEventEmitter() {
  const listeners = new Map(); // Private
 
  return {
    on(event, callback) {
      if (!listeners.has(event)) {
        listeners.set(event, []);
      }
      listeners.get(event).push(callback);
      return this;
    },
 
    off(event, callback) {
      if (!listeners.has(event)) return;
      const fns = listeners.get(event);
      listeners.set(event, fns.filter((fn) => fn !== callback));
      return this;
    },
 
    emit(event, ...args) {
      if (!listeners.has(event)) return;
      listeners.get(event).forEach((fn) => fn(...args));
    },
 
    once(event, callback) {
      const wrapper = (...args) => {
        callback(...args);
        this.off(event, wrapper);
      };
      this.on(event, wrapper);
      return this;
    }
  };
}
 
const emitter = createEventEmitter();
emitter.on("message", (text) => console.log("Got:", text));
emitter.once("connect", () => console.log("Connected!"));
 
emitter.emit("connect");   // "Connected!"
emitter.emit("connect");   // (nothing - was once)
emitter.emit("message", "Hello"); // "Got: Hello"

Pattern Summary Table

PatternClosed-Over StateUse Case
DebouncetimerIdSearch input, resize handler
Throttlewaiting, lastArgsScroll, mouse move, rate limiting
Oncecalled, resultInitialization, one-time setup
Curryargs accumulatorLogging, config, reusable pipelines
PartialpresetArgsAPI clients, pre-configured functions
Iteratorcurrent positionCustom iteration, pagination
State Machinecurrent stateUI flows, traffic lights, game states
Event Emitterlisteners mapPub/sub, event systems
Rune AI

Rune AI

Key Insights

  • Debounce delays execution until input stops: It clears and resets a timer on each call, so the wrapped function only fires when the user pauses
  • Throttle limits execution frequency: It guarantees the function runs at most once per time window, saving a trailing call for the latest arguments
  • Once captures a single result: The first call executes and caches the result, all subsequent calls return the cache without executing again
  • Currying and partial application pre-fill arguments: Both store provided arguments in closure scope and return a function that waits for the remaining ones
  • State machines use closures for private transitions: The current state and transition map live inside the closure, exposing only controlled methods to the outside
RunePowered by Rune AI

Frequently Asked Questions

What is the most common real-world use of closures?

Debounce and throttle are the most widely used closure patterns. Nearly every web application uses debounced search inputs and throttled scroll handlers. [Event listeners](/tutorials/programming-languages/javascript/how-to-add-event-listeners-in-js-complete-guide) themselves are closures when they reference variables from their enclosing scope, making closures appear in virtually every interactive feature.

How does debounce work under the hood?

Debounce stores a `timerId` in a closure variable. Each time the function is called, it clears the previous timer with `clearTimeout` and sets a new one with `setTimeout`. The actual function only fires when enough time passes without another call. The [callback](/tutorials/programming-languages/javascript/what-is-a-callback-function-in-js-full-tutorial) function and delay are also captured in the closure scope.

What is the difference between currying and partial application?

Currying transforms a function into a chain where each function takes exactly one argument: `f(a)(b)(c)`. Partial application pre-fills some arguments and returns a function that takes the remaining ones: `g(b, c)` where `a` was already provided. Both use closures to store the pre-supplied arguments. In practice, JavaScript developers use both interchangeably.

Can closures replace classes for managing state?

Yes, and in many cases they should. Closures provide true data privacy (no way to access the hidden variables from outside), while class fields with `#` prefixes are a newer feature. For simple state management like counters, toggles, or configuration objects, a closure-based factory function is often simpler and more predictable than a class. For complex object hierarchies with inheritance, classes remain the better choice.

Are these closure patterns already built into JavaScript?

Some are built into libraries. Lodash provides `_.debounce`, `_.throttle`, `_.once`, `_.curry`, and `_.partial`. But understanding how to build them yourself helps you customize behavior (like the immediate debounce variant) and debug issues when the closure state behaves unexpectedly.

Conclusion

Closures power the most important utility patterns in JavaScript. From debounce and throttle to currying, once-wrappers, state machines, and event emitters, the pattern is always the same: a function returns another function that captures private variables in its scope.