Kata: once
Keep the flag apart from the result, and raise it only when fn has returned.
Predict first
Before you read: predict
ExamHall wraps its camera set-up as initProctoring = once(requestCamera) at the start of each attempt. requestCamera() shows the browser's camera prompt and returns the camera stream. Three components call initProctoring() while the page mounts. At the first call another app holds the camera, so requestCamera throws; from the second call on, the camera is free. The team's kata answer:
How many camera prompts does the candidate see, and what do the second and third calls get?
export function once(fn) { let called = false; let result; return function (...args) { if (called) return result; called = true; result = fn.apply(this, args); return result; };}The 30-second version
A kata once closes over two variables: a called flag and the result. The first call runs fn.apply(this, args), stores what returns, and only then raises the flag, so a throw leaves the flag down and the next call tries again. Every later call returns the stored result, undefined included, and ignores its own arguments.
Keep the flag apart from the result, and raise it only when fn has returned.
How it works
Step 1 of 13
fn be async?Analogy: A bell foundry's casting book (metal crafts)
A bell is cast once for each order, and the founder keeps the casting book.
| At the foundry | In the kata |
|---|---|
| one casting-book page per bell order, never shared with the next order | one called flag per once() call (step 3) |
| the page says "cast", whether or not the bell carries an inscription | a flag kept apart from the result (step 4) |
| the founder writes "cast" only after the bell comes out of the mould whole | store, then flag (step 6) |
| a mould that cracks during the pour: no entry, and the foundry pours again | a throw is not counted (step 6) |
| a churchwarden who calls later is told "already cast" and shown that bell | later calls get the first result (step 7) |
| one page used for two churches' orders: the second church never gets a bell | once on a prototype method (step 9) |
In real terms: once is a flag and a stored value in front of one function, kept by a closure.
Breaks down when: a cracked mould is plain at once, while a promise that later rejects looks like a normal return at the moment of the call (step 8).
Contrasting case: a founder who writes "cast" as the metal starts to pour: a mould that cracks leaves an order logged as cast with no bell (the flag first, step 11).
Examples, run for real
4.1 ExamHall EXH-3510: three camera prompts per attempt
Proctoring wrapped initProctoring = once(requestCamera) with a draft that checked if (!result). requestCamera() stores the stream in the attempt's state and returns nothing, and three components call it while the page mounts (EXH-3510). MR !3810 replaced the draft with the reference and added the test list to the repository. The list also runs on three variants: M1 checks if (!result); M2 is the flag raised before fn runs (Lodash's choice), which fails this contract's T7 and T8; M3 returns an arrow function.
4.1 reference.mjs 10 of 10 pass
4.1 M1, if (!result) as the check 8 of 10 pass · red: T5 "an undefined result still counts: fn runs once", T6 "a falsy result (0, empty string, false, null) is returned and fn runs once"
4.1 M2, the flag raised before fn runs 8 of 10 pass · red: T7 "a call that throws is not counted: the next call runs fn again", T8 "the throwing call's error reaches its own caller"
4.1 M3, an arrow function returned 9 of 10 pass · red: T4 "fn gets the caller's this"
4.1 EXH-3510 the M1 draft, one attempt, 3 components mounting: 3 camera prompts
4.1 MR !3810 the reference, the same attempt: 1 camera prompt
M1 runs fn again for every falsy result (step 4), so each mount asks for the camera three times. M2 counts a throw and hands later callers an empty result (step 6). M3's arrow takes the module's this, not the caller's (step 5).
4.2 Chat: a topic set twice
A chat room sets its topic once, as setTopic = once(saveTopic). The owner types "Launch", then edits it to "Launch v2":
4.2 setTopic('Launch') then setTopic('Launch v2'): saveTopic runs 1 time, with 'Launch'
4.2 the second call returns the first call's object (same reference: true), topic 'Launch'
The second call's argument is never seen; it gets the first call's result (step 7). A room that must accept edits needs a different function, not once.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A chat thread view wraps three loaders with this kata answer of once:
fetchDraft() returns the thread's saved draft text, '' here (none saved). fetchLastRead() returns the id of the last message read, as text: '0' here (none read yet). fetchPinned() is async, and its promise fulfils with null here (nothing pinned). While the view mounts, it calls each wrapped function three times, and waits for a returned promise to settle before the next call.
Which of the three wrapped functions run more than once?
export function once(fn) { let result; return function (...args) { if (result) return result; result = fn.apply(this, args); return result; };} export const loadDraft = once(fetchDraft);export const loadLastRead = once(fetchLastRead);export const loadPinned = once(fetchPinned);Question 2 of 8
A chat client opens its socket with connect = once(openSocket), and openSocket throws while the device is offline. A candidate's answer tags any error from fn before passing it on, and is complete apart from two lines:
The team's list has 10 tests.
Which two lines pass all 10?
export function once(fn) { let called = false; let result; return function (...args) { if (called) return result; try { /* the two lines */ } catch (error) { error.fromOnce = true; throw error; } return result; };}Question 3 of 8
A chat client joins rooms with joinRoom = once(sendJoin). The interviewer's contract: run fn at most once, give every later call its first result, forward this and the arguments, and do not count a call that threw. The skeleton:
Put the four lines of the returned function's body in order, the first line first.
Order these chips: called = true; return result; if (called) return result; result = fn.apply(this, args);
export function once(fn) { let called = false; let result; return function (...args) { /* the four lines */ };}Your order
- 1empty
- 2empty
- 3empty
- 4empty
Tap the lines in the order you expect.
Question 4 of 8
A chat page starts through one entry point, init; once is the kata's reference answer:
The sidebar's constructor calls init() too, because the sidebar can also be mounted on its own. The page calls init() once.
What happens?
import { once } from './once.mjs'; export const log = []; export const init = once(setupChat); function setupChat() { log.push('setupChat'); new Sidebar(); return 'ready';} class Sidebar { constructor() { init(); }}Question 5 of 8
the interviewer says: "Implement once(fn). It runs fn at most once and gives every later call the first result. Forward this and the arguments, and don't count a call that threw." You have 15 minutes and a plain editor, and ExamHall's initProctoring is the use case.
Before you type: say your test list out loud.
Name at least seven tests, each with its calls and what it expects.
Question 6 of 8
the interviewer says: "Implement once(fn). It runs fn at most once and gives every later call the first result. Forward this and the arguments, and don't count a call that threw." You have 15 minutes and a plain editor, and ExamHall's initProctoring is the use case.
Now write it, in 15 minutes, from a blank file that exports once.
Question 7 of 8
A chat client loads a thread with loadThread = once(fetchThread); once is the kata's reference answer:
fetchThread() returns a promise that rejects when the server answers with an error. The first fetch gets a 503, and the server is back from then on. The view calls loadThread(), waits until that promise has settled, then calls loadThread() again.
How many fetches run, and what does the second call get?
import { once } from './once.mjs'; export const loadThread = once(fetchThread);Question 8 of 8
A payments page's Pay button must submit once, even on a double-click, and submitPayment(event) reads the click event. once is the kata's reference answer:
Which line, in place of the comment, sends one payment for a double-click and hands submitPayment the click event?
import { once } from './once.mjs'; export function wire(payButton, submitPayment) { /* the line */}Go deeper
Pro tips
- Run your own answer against the list before you call it done:
KATA_IMPL=./my-once.mjs node --test kata.test.mjs. When: practising the kata at home, against the same tests the reference passes (step 13). Verify (RAN,tips.mjs):tip 1 KATA_IMPL=./reference.mjs: exit 0 · tests 10 · pass 10 · fail 0 tip 1 KATA_IMPL=<temp>/m2-once.mjs (the flag raised before fn runs): exit 1 · tests 10 · pass 8 · fail 2 · red: T7, T8 - When the interviewer turns to memory, offer to drop
fnafter its call: setfn = undefinedonce the result is stored, so the closure no longer keeps it alive. When: after the core tests pass, as a production follow-up (step 3). Verify (RAN,tips.mjs): the reference with that one line added still passes the list.tip 2 the reference with `fn = undefined;` after `called = true;`: 10 of 10 pass
Source
Vendor and guide pages change, so their claims are labelled as of 2026-10-12; every behaviour this lesson grades is executed on Node 24.
- Lodash 4.17.15 documentation,
_.once(the curriculum row's first source), https://lodash.com/docs/4.17.15#once. lodash.com timed out on 2026-10-12 (curl exit 28, twice), so the page was read from the unmodified Internet Archive capture (id_, Memento-Datetime 2025-12-05T10:14:29Z) of that URL, as_fix-brief-r1.mdruling 1 accepts for a named primary page (lodash-once.txt, header linesVia,Memento-DatetimeandRaw-SHA256). It says that "Repeat calls to the function return the value of the first invocation" (step 7) and that "The func is invoked with the this binding and arguments of the created function" (step 5). Lodash keys no item; this lesson's reference is its own code, with a different contract on a throw (next source). - Lodash 4.18.1 source,
once.jsandbefore.js(npm packagelodash@4.18.1, copied unmodified from the local install the F3 review'sonce-lodash.mjsruns;lodash-source.txt, headerFile-SHA256).once.jsis "return before(2, func);", andbefore.jstests "if (--n > 0) {" before it runs "result = func.apply(this, arguments);". The count goes down beforefuncruns, so a call that threw has used up the one call: Lodash's_.oncecounts a throw, and later calls get the storedresult, never assigned. This lesson's contract (step 2) is the other side of step 12's trade-off. - MDN, Function.prototype.apply() (the row's second source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply (
mdn-apply.txt): it "calls this function with a given this value, and arguments provided as an array (or an array-like object)", and returns "The result of calling the function with the specified this value and arguments" (step 4). As of 2026-10-12. - Front End Interview Handbook, JavaScript coding, https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt): candidates take "around 10-15 minutes for a basic question" (the time box, step 1), and it advises "writing test cases" (step 13). As of 2026-10-12.
Related lessons
Checked against the tools and docs as of 2026-10-12.