You've wired up an init() that sets up listeners, opened a "Save" handler that creates a database row, or shipped an analytics call you'd be embarrassed to fire twice. once is the standard guard: it wraps a function so it runs the first time you call it and then turns into a no-op that returns the original result forever after.
Implement once(fn). It accepts any function fn and returns a new function. The first time the returned function is called, it invokes fn with the arguments you passed and remembers the return value. Every subsequent call ignores its arguments and returns the remembered value — without calling fn again.
function once(fn) {
// returns a new function. The first call invokes fn(...args) and
// caches the result. Every later call returns the cached result
// without calling fn again.
}
let calls = 0;
const init = once(() => {
calls++;
return 'ready';
});
init(); // 'ready' (calls === 1)
init(); // 'ready' (calls === 1, fn was not called again)
init(); // 'ready' (calls === 1)
const add = once((a, b) => a + b);
add(2, 3); // 5 — fn runs with (2, 3)
add(10, 7); // 5 — args ignored, cached result returned
fn; later calls ignore whatever you pass them.this binding — when the returned function is called as a method (obj.cb()), fn should see obj as this on the first call.fn returns, including undefined, null, or 0 — all are valid cached results.fn, even with different arguments.once(fn) creates a fresh, independent wrapper.You'll build a wrapper that flips a switch the first time it runs and short-circuits every call after that.
Some functions should run exactly once. Think of an init() that wires up DOM listeners, a connect() that opens a single socket, or an analytics track('signup') you'd be embarrassed to fire twice. You don't want callers to remember "did I already call this?" — you want the function itself to refuse to run a second time and just hand back whatever it returned the first time.
That's once: it takes any function fn and returns a new function. First call: run fn, remember the result. Every later call: return that remembered result, don't touch fn again.
The returned wrapper has two states and exactly one transition between them. Before the first call it's uncalled — when invoked, it runs fn, stores the result, and flips to called. From then on, no matter how many times you invoke it (or with what arguments), it just returns the stored value. There is no path back to uncalled. It's a one-way switch.
To make that switch survive across calls, you need a place to keep it that's outside the returned function itself. That place is a closure — variables declared in the outer function (once) that the inner function (the wrapper) can read and write across every invocation.
A reasonable first try is to put the flag inside the returned function:
function onceBroken(fn) {
return function (...args) {
let called = false; // resets on every call — that's the bug
let value;
if (!called) {
value = fn(...args);
called = true;
}
return value;
};
}
This looks right — there's a flag, there's a check, there's a stored value. But let called = false is declared inside the returned function, so every invocation creates a fresh called initialized to false. The check never sees a stale true. fn runs every single time, and once becomes "wrap a function in two extra lines that do nothing."
The fix is to move the state out of the call and into the surrounding scope, so every call shares the same flag.
function once(fn) {
// Two pieces of state live in the OUTER scope — declared here, before the
// returned function. Every call to the wrapper sees the same `called` and
// the same `value`, because closures share the variables of their parent.
let called = false;
let value;
// Use `function` (not an arrow) so that when the wrapper is called as a
// method — obj.cb() — `this` inside refers to obj. An arrow would freeze
// `this` to whatever it was when `once` ran, which is almost never what
// the caller wants.
return function (...args) {
if (!called) {
// First call only: flip the flag BEFORE invoking fn, then store the
// result. (Order matters slightly: see "Gotchas" below for why.)
called = true;
value = fn.apply(this, args);
}
return value;
};
}
module.exports = { once };
The shift from the naive version is small but the consequences are large: let called = false and let value now live in once's scope, not the wrapper's. They're created exactly once — the moment you call once(fn) — and they persist for the lifetime of the returned function. The if (!called) check finally has something stable to look at.
A few specific lines worth pausing on:
let value; with no initializer — value starts as undefined. That's deliberate: if fn returns undefined (a perfectly valid return value), we still want to return that on call #2, not "the placeholder we used before fn ran." Because we gate on called, not on whether value is truthy, an undefined result caches just as cleanly as 'ready' or 42.fn.apply(this, args) instead of fn(...args) — this is the mechanism that preserves the caller's this. fn.apply(this, args) forwards both the arguments AND the this binding from the caller in one shot; fn(...args) forwards only the arguments and loses this entirely. Concretely: if someone writes obj.cb = once(fn); obj.cb(), the wrapper sees this === obj, and apply hands that same obj to fn. Swap in fn(...args) and fn would see this === undefined in strict mode (or the global object in sloppy mode) — any obj.cb() call that relied on this.something would break. .apply is what makes once transparent to methods.called = true before fn.apply(...) — if fn throws, the flag is already set, so the wrapper won't retry on the next call. This matches lodash's behavior and the common spec: a thrown call counts as "used up." If you wanted the opposite — retry until fn succeeds — swap the two lines.Take the example from the description:
let calls = 0;
const init = once(() => {
calls++;
return 'ready';
});
When you wrote once(...), the outer scope was created: called = false, value = undefined. The wrapper was returned and assigned to init. calls is 0 and fn has not run.
init() (first call) — wrapper runs. called is false, so we enter the if branch. Set called = true, then call fn with no args. Inside fn: calls goes from 0 to 1, returns 'ready'. Back in the wrapper, value = 'ready'. Return value. Caller sees 'ready'.init() (second call) — wrapper runs. called is true, so we skip the if block entirely. fn is never touched; calls stays at 1. We just return value, which is 'ready'. Caller sees 'ready' — same string, no side effects.init() (third call) — identical to the second. calls is still 1. Return 'ready'.The visible side effect (calls++) happens exactly once. The visible return value ('ready') is the same across all three calls. That's what once guarantees.
called inside the returned function — every call resets it to false, so fn runs every time. If you wrote return function () { let called = false; ... }, calling init() three times would set calls to 3, not 1. State for "have I run yet?" must live in the outer scope.return (...args) => fn(...args) looks tidy but breaks this forwarding. With obj.cb = once(fn); obj.cb(), the wrapper would see whatever this was when once ran (usually undefined in modules), and fn would never get obj. Use function () { ... } and fn.apply(this, args).value instead of called — if you wrote if (value === undefined) { value = fn(...) }, then a fn that legitimately returns undefined would be re-run on every call (because the cache check never sees a "real" value). Concrete example: const ready = once(() => undefined); ready(); ready(); would invoke fn twice with the value-gated version and once with the called-gated version. Always gate on the boolean flag.called = true after fn throws — if called = true runs after fn.apply(...), then a throw inside fn leaves called as false, and the next call re-runs fn. Set the flag before invoking fn so a throw still counts as "used up." (Or, if your spec says "retry on throw," swap the order — just make the choice deliberately.)once wrappers — once(a) and once(b) must be independent. If you accidentally pulled called and value out to module scope instead of putting them inside once, both wrappers would share one flag. The example const a = once(...); const b = once(...); a(); b(); would then run a's fn on a() and skip b's fn entirely on b(). Keep the state inside once, so each invocation of once creates its own pair.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You've wired up an init() that sets up listeners, opened a "Save" handler that creates a database row, or shipped an analytics call you'd be embarrassed to fire twice. once is the standard guard: it wraps a function so it runs the first time you call it and then turns into a no-op that returns the original result forever after.
Implement once(fn). It accepts any function fn and returns a new function. The first time the returned function is called, it invokes fn with the arguments you passed and remembers the return value. Every subsequent call ignores its arguments and returns the remembered value — without calling fn again.
function once(fn) {
// returns a new function. The first call invokes fn(...args) and
// caches the result. Every later call returns the cached result
// without calling fn again.
}
let calls = 0;
const init = once(() => {
calls++;
return 'ready';
});
init(); // 'ready' (calls === 1)
init(); // 'ready' (calls === 1, fn was not called again)
init(); // 'ready' (calls === 1)
const add = once((a, b) => a + b);
add(2, 3); // 5 — fn runs with (2, 3)
add(10, 7); // 5 — args ignored, cached result returned
fn; later calls ignore whatever you pass them.this binding — when the returned function is called as a method (obj.cb()), fn should see obj as this on the first call.fn returns, including undefined, null, or 0 — all are valid cached results.fn, even with different arguments.once(fn) creates a fresh, independent wrapper.You'll build a wrapper that flips a switch the first time it runs and short-circuits every call after that.
Some functions should run exactly once. Think of an init() that wires up DOM listeners, a connect() that opens a single socket, or an analytics track('signup') you'd be embarrassed to fire twice. You don't want callers to remember "did I already call this?" — you want the function itself to refuse to run a second time and just hand back whatever it returned the first time.
That's once: it takes any function fn and returns a new function. First call: run fn, remember the result. Every later call: return that remembered result, don't touch fn again.
The returned wrapper has two states and exactly one transition between them. Before the first call it's uncalled — when invoked, it runs fn, stores the result, and flips to called. From then on, no matter how many times you invoke it (or with what arguments), it just returns the stored value. There is no path back to uncalled. It's a one-way switch.
To make that switch survive across calls, you need a place to keep it that's outside the returned function itself. That place is a closure — variables declared in the outer function (once) that the inner function (the wrapper) can read and write across every invocation.
A reasonable first try is to put the flag inside the returned function:
function onceBroken(fn) {
return function (...args) {
let called = false; // resets on every call — that's the bug
let value;
if (!called) {
value = fn(...args);
called = true;
}
return value;
};
}
This looks right — there's a flag, there's a check, there's a stored value. But let called = false is declared inside the returned function, so every invocation creates a fresh called initialized to false. The check never sees a stale true. fn runs every single time, and once becomes "wrap a function in two extra lines that do nothing."
The fix is to move the state out of the call and into the surrounding scope, so every call shares the same flag.
function once(fn) {
// Two pieces of state live in the OUTER scope — declared here, before the
// returned function. Every call to the wrapper sees the same `called` and
// the same `value`, because closures share the variables of their parent.
let called = false;
let value;
// Use `function` (not an arrow) so that when the wrapper is called as a
// method — obj.cb() — `this` inside refers to obj. An arrow would freeze
// `this` to whatever it was when `once` ran, which is almost never what
// the caller wants.
return function (...args) {
if (!called) {
// First call only: flip the flag BEFORE invoking fn, then store the
// result. (Order matters slightly: see "Gotchas" below for why.)
called = true;
value = fn.apply(this, args);
}
return value;
};
}
module.exports = { once };
The shift from the naive version is small but the consequences are large: let called = false and let value now live in once's scope, not the wrapper's. They're created exactly once — the moment you call once(fn) — and they persist for the lifetime of the returned function. The if (!called) check finally has something stable to look at.
A few specific lines worth pausing on:
let value; with no initializer — value starts as undefined. That's deliberate: if fn returns undefined (a perfectly valid return value), we still want to return that on call #2, not "the placeholder we used before fn ran." Because we gate on called, not on whether value is truthy, an undefined result caches just as cleanly as 'ready' or 42.fn.apply(this, args) instead of fn(...args) — this is the mechanism that preserves the caller's this. fn.apply(this, args) forwards both the arguments AND the this binding from the caller in one shot; fn(...args) forwards only the arguments and loses this entirely. Concretely: if someone writes obj.cb = once(fn); obj.cb(), the wrapper sees this === obj, and apply hands that same obj to fn. Swap in fn(...args) and fn would see this === undefined in strict mode (or the global object in sloppy mode) — any obj.cb() call that relied on this.something would break. .apply is what makes once transparent to methods.called = true before fn.apply(...) — if fn throws, the flag is already set, so the wrapper won't retry on the next call. This matches lodash's behavior and the common spec: a thrown call counts as "used up." If you wanted the opposite — retry until fn succeeds — swap the two lines.Take the example from the description:
let calls = 0;
const init = once(() => {
calls++;
return 'ready';
});
When you wrote once(...), the outer scope was created: called = false, value = undefined. The wrapper was returned and assigned to init. calls is 0 and fn has not run.
init() (first call) — wrapper runs. called is false, so we enter the if branch. Set called = true, then call fn with no args. Inside fn: calls goes from 0 to 1, returns 'ready'. Back in the wrapper, value = 'ready'. Return value. Caller sees 'ready'.init() (second call) — wrapper runs. called is true, so we skip the if block entirely. fn is never touched; calls stays at 1. We just return value, which is 'ready'. Caller sees 'ready' — same string, no side effects.init() (third call) — identical to the second. calls is still 1. Return 'ready'.The visible side effect (calls++) happens exactly once. The visible return value ('ready') is the same across all three calls. That's what once guarantees.
called inside the returned function — every call resets it to false, so fn runs every time. If you wrote return function () { let called = false; ... }, calling init() three times would set calls to 3, not 1. State for "have I run yet?" must live in the outer scope.return (...args) => fn(...args) looks tidy but breaks this forwarding. With obj.cb = once(fn); obj.cb(), the wrapper would see whatever this was when once ran (usually undefined in modules), and fn would never get obj. Use function () { ... } and fn.apply(this, args).value instead of called — if you wrote if (value === undefined) { value = fn(...) }, then a fn that legitimately returns undefined would be re-run on every call (because the cache check never sees a "real" value). Concrete example: const ready = once(() => undefined); ready(); ready(); would invoke fn twice with the value-gated version and once with the called-gated version. Always gate on the boolean flag.called = true after fn throws — if called = true runs after fn.apply(...), then a throw inside fn leaves called as false, and the next call re-runs fn. Set the flag before invoking fn so a throw still counts as "used up." (Or, if your spec says "retry on throw," swap the order — just make the choice deliberately.)once wrappers — once(a) and once(b) must be independent. If you accidentally pulled called and value out to module scope instead of putting them inside once, both wrappers would share one flag. The example const a = once(...); const b = once(...); a(); b(); would then run a's fn on a() and skip b's fn entirely on b(). Keep the state inside once, so each invocation of once creates its own pair.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.