Implement `once(fn)`: a higher-order function returning a wrapper that runs `fn` at most one time and returns that first result on every later call. What must the wrapper get right?
answer
- closure holds flag plus cached result
- forward every argument through
- return the cached value every time
- flip the flag before invoking
- decide what happens if fn throws
basics
~20 sReturn a wrapper holding a called flag and a cached result: on the first call it forwards every argument to fn, stores the return value and flips the flag; afterwards it skips fn and returns the cached value. Forwarding arguments and returning the value are the parts people drop.
solid answer
~50 sThe shape is small: `const once = (fn) => { let done = false, result; return (...args) => { if (!done) { done = true; result = fn(...args); } return result; }; };`. Three things matter beyond "call it once". First, set the flag *before* invoking `fn`, so a reentrant call from inside `fn` cannot trigger a second run. Second, forward all arguments with rest and spread rather than hard-coding one parameter, since the wrapper stands in for the original at any call site. Third, return `result` on every call, not just the first — a wrapper that silently returns `undefined` on the second call is the classic bug. If `fn` may hold a large closure you no longer need, setting `fn = null` after the single call lets it be collected. Note the wrapper caches by invocation, not by argument, so later calls with different arguments still get the first result.
code
javascript · 24 linesfunction once(fn) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true; // before the call: blocks reentrancy
result = fn(...args);
}
return result; // every call, not just the first
};
}
let runs = 0;
const load = once((name) => {
runs += 1;
return { name, id: runs };
});
const first = load('config');
const second = load('ignored');
console.log(runs); // 1
console.log(second); // { name: 'config', id: 1 }
console.log(first === second); // true
console.log(load.length); // 0 - the wrapper only declares ...argsgo deeper
Be able to write the four-line version from memory: a flag, a stored result, forward the arguments, return the stored result. Say out loud that the second call must still return something useful.
Explain each part of the contract — argument forwarding, return-value caching, flag ordering — and identify the buggy variant that returns undefined on later calls when it is shown to you.
Demonstrate the judgment calls: what happens on throw, whether the cached value should be the same object identity, and when releasing the reference to fn matters in a long-lived process.
Own the wrapper-fidelity question in general: what a decorator must preserve to be a safe drop-in — arguments, return value, error behaviour, introspectable metadata — and how a house convention for wrappers prevents a family of subtle bugs across a codebase.
## Why this question is asked `once` is the smallest useful *wrapper*: a higher-order function that takes a function and returns a function with the same calling shape but modified behaviour. Interviewers like it because it is four lines, has a clear specification, and has three separate places to slip. ## A correct implementation ```js function once(fn) { let called = false; let result; return (...args) => { if (!called) { called = true; // set BEFORE invoking result = fn(...args); } return result; // on every call, not just the first }; } const init = once((label) => { console.log('initialising', label); return { ready: true }; }); const a = init('boot'); // logs once, returns { ready: true } const b = init('again'); // logs nothing, returns the SAME object console.log(a === b); // true ``` ## The three things to get right **Forward the arguments.** The wrapper is a stand-in for `fn`, so it must accept whatever callers pass. `(...args) => fn(...args)` forwards any number of arguments. Writing `(x) => fn(x)` quietly truncates every call to one argument, which breaks the moment someone passes two. **Return the cached value on every call.** A tempting version is: ```js // BUG: returns undefined from the second call onward return (...args) => { if (!called) { called = true; return fn(...args); } }; ``` This satisfies "runs once" and violates the more useful part of the contract. Callers that treat `init()` as idempotent — "give me the initialised config, I do not care who initialised it" — now get `undefined` on every call after the first. Whether identity matters is a design decision worth stating: returning the *same* object each time is usually the point. **Guard against reentrancy.** Set the flag before calling `fn`, not after. If `fn` synchronously calls the wrapper again — directly, or through some listener it registers — a flag set afterwards has not been assigned yet, and `fn` runs a second time, possibly recursing. Ordering the two statements correctly costs nothing and removes the hazard. On the reentrant inner call the wrapper returns the current value of `result`, which is still `undefined` because the outer call has not returned yet; that is the honest behaviour, and it is far better than infinite recursion. ## Optional refinements Releasing the reference lets the engine collect anything `fn` was holding: ```js function once(fn) { let result; return (...args) => { if (fn) { result = fn(...args); fn = null; } return result; }; } ``` Here `fn` doubles as the flag. It is neat, but it fails for a caller who legitimately passes `null`-ish values around, and it makes the code slightly harder to read; both versions are defensible in an interview as long as you can say why you chose one. What about a throwing `fn`? With the version above, if `fn` throws, `called` is already `true`, so the wrapper will never retry and every later call returns `undefined`. That may be exactly what you want for a one-shot side effect, or exactly wrong for a lazy initialiser you would like to retry. State the choice out loud: "failures are not retried" is a specification, not an oversight. If retrying is desired, set the flag in a `finally` only on success, or catch and reset. ## Where it is used `once` shows up wherever an action must not be repeated: one-shot initialisation, a cleanup routine that several code paths may reach, a callback that must fire exactly one time even if two events race, a warn-once diagnostic in a library. It is also the seed of a family of related wrappers — a counter-limited `atMost(n, fn)`, a `throttle`, a `memoize` — all built on the same skeleton of "closure holds state, returned function consults it, arguments and return value are forwarded faithfully". ## The wrapper-fidelity principle The general lesson generalises past `once`: when you return a wrapper, decide deliberately what it preserves. Arguments and return value are the minimum. Beyond that, the wrapper is a *different* function object — `wrapped.name` is not `fn.name`, `wrapped.length` is 0 because it declares only a rest parameter, and any properties you attached to `fn` are gone. That rarely matters, but when code introspects functions it matters a lot, and saying so unprompted is what separates a competent answer from a good one.
- What should happen if `fn` throws on its first call?You must decide and say so. With the flag set before invocation, a throw leaves the wrapper permanently spent: later calls return `undefined` and never retry. For a one-shot side effect that is right. For a lazy initialiser you usually want retry, which means marking success only after `fn` returns — and accepting that a throwing reentrant call could then run `fn` twice.
- Why set the `called` flag before invoking `fn` rather than after?Because `fn` may synchronously call the wrapper again — directly, or via something it registers. If the flag is only set after `fn` returns, that reentrant call sees `called === false` and runs `fn` a second time, potentially recursing without end. Setting it first makes the reentrant call a cheap no-op returning the not-yet-assigned result.
- What does the returned wrapper fail to preserve about the original function?Its identity and metadata. The wrapper is a new function object: `name` is not the original's, `length` is 0 because it declares only a rest parameter, and any properties attached to `fn` are not visible through it. Code that introspects functions — dispatch tables keyed by name, arity checks — can be surprised, so copy what you need deliberately.
saying these in an interview costs you the question
- Returns undefined on every call after the first
- Hard-codes one parameter instead of forwarding all arguments
- Sets the called flag after invoking fn
- Assumes different arguments produce a different cached result
- Claims a module-level boolean is equivalent for many wrapped functions