once(fn) returns a function that runs fn at most one time — the first call executes it, and every subsequent call returns the cached result. It's a single-closure problem that tests whether you understand function wrapping and state encapsulation.
What is once()?
once(fn) is a higher-order function that wraps another function and returns a version that executes at most one time. The first call runs fn and caches the result; every subsequent call returns the cached result without invoking fn again.
This is a call-limiting pattern, not a caching pattern. You're not memoizing by input — you're memoizing by the fact of having been called. The arguments on subsequent calls are irrelevant; you always get back the first result.
Real-world use cases:
- Singleton initialization —
const initApp = once(() => { setupEventListeners(); loadConfig(); })ensures setup runs exactly once, no matter how many times it's triggered - One-time event handlers — a click handler that should fire the first time and become a no-op after
- Lazy computation — defer an expensive calculation until first access, then cache forever
- Payment/submission buttons — prevent double-submission by wrapping the submit handler in
once()
The implementation is a single closure with two variables: a called flag and a result cache. The only nuance is forwarding this so the wrapped function sees the caller's context. The interviewer will often extend this to limit(fn, n) — generalizing from "at most once" to "at most N times."
The Problem
"Implement once(fn) — a function that takes another function and returns a new function. The returned function calls fn only on the first invocation and returns the same result for all subsequent calls."
The interviewer may also ask:
"Now implement limit(fn, n) — similar to once, but the function can be called up to n times before being locked."
Thought Process
You need three things:
- A flag (or counter) to track whether the function has been called
- Storage for the cached result
- A returned function that checks the flag, calls
fnif allowed, and returns the cached result otherwise
The closure captures the flag and the cache — that's the entire pattern.
The key question the interviewer wants to hear you ask: "What should the returned function return on subsequent calls — undefined or the cached result?" The answer: the cached result from the first call. That's what makes once useful.
Step 1 — Base Implementation
Loading editor...
Step 2 — Preserving this Context
The interviewer will ask: "What if the wrapped function uses this?"
Loading editor...
Step 3 — Extension: limit(fn, n)
The interviewer extends the question: "Now generalize it. Instead of just once, let the function be called at most n times."
Loading editor...
Step 4 — Edge Cases
What if fn throws? The native once behavior depends on the implementation. A reasonable choice: if fn throws, don't cache the result and don't mark it as called — let the caller retry. Or mark it as called anyway but return undefined. State your choice and why.
What if fn returns undefined? This is the classic cache-miss problem. If the function legitimately returns undefined, we still need to know the function was called. That's why we use a separate called flag rather than checking result === undefined.
Async functions: once works with async functions, but the cached result will be a Promise. Subsequent calls return the same Promise — not necessarily the resolved value. If you need the resolved value cached, you'd need to await the result first.
Full Solution
Loading editor...
What Interviewers Are Testing
- Closure for state — using a closed-over variable (
called) as private state - Function wrapping — the pattern of taking a function, wrapping it with behavior, and returning a new function
thisforwarding — using.apply(this, args)so the wrapped function sees the correct context- Flag vs result check — understanding why a boolean flag is safer than checking
result === undefined
Complexity
| Time | Space | |
|---|---|---|
| First call | O(T) of fn | O(1) + O(S) of fn |
| Subsequent calls | O(1) | O(1) |
Interview Tips
- State the caching behavior upfront — "I'll use a closure to track whether the function has been called and cache the result." This shows you've seen the pattern before.
- Ask about error handling — "If
fnthrows, should the wrapper mark it as called, or allow a retry?" Asking this shows you think about edge cases without being prompted. - Write
oncefirst, then generalize —limit(fn, 1)isonce(fn). Show the generalization naturally by replacing the boolean with a counter. - Mention
thisforwarding — even if the test case doesn't usethis, use.apply(this, args)and briefly note why. It shows you think about real-world usage.