skip to content

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?

level: middleimportance: should knowfreq 45%

answer

  1. closure holds flag plus cached result
  2. forward every argument through
  3. return the cached value every time
  4. flip the flag before invoking
  5. decide what happens if fn throws

basics

~20 s

Return 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 s

The 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 lines
javascript
function 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 ...args

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context