Run a Function Only Once
Wrapping a function so it runs at most once is the right tool for one-shot initialisers, lazy connection setup, and event handlers that must not double-fire. This snippet covers the canonical `once`, an async-aware version that caches the in-flight Promise, and a `resetOnce` variant for tests and feature flags. Drop it next to your event listeners and stop guarding with ad-hoc booleans.
877 views
22
function once(fn) {
let called = false;
let result;
return function onceCaller(...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
};
}
const init = once(() => {
console.log('initialising...');
return { ready: true };
});
console.log(init()); // initialising... { ready: true }
console.log(init()); // { ready: true } (no second log)
console.log(init()); // { ready: true }The implementation is three closures: a called flag, a cached result, and the wrapper itself. After the first invocation, the wrapper returns the cached result without calling fn again, so subsequent calls are O(1) and side-effect-free. The early return result matches what callers expect: they get the same value every time, even if they forget that the work has already happened. This pairs nicely with module-level init functions, single-shot bootstrap routines, and event handlers like a click that should only ever fire once.
function onceAsync(fn) {
let pending = null;
return function onceAsyncCaller(...args) {
if (pending) return pending;
pending = Promise.resolve()
.then(() => fn.apply(this, args))
.catch((err) => {
pending = null; // allow retry on failure
throw err;
});
return pending;
};
}
let calls = 0;
const connect = onceAsync(async () => {
calls += 1;
return { socket: 'open' };
});
(async () => {
await Promise.all([connect(), connect(), connect()]);
console.log('actual connect calls:', calls);
// actual connect calls: 1
})();Async initialisers (DB pool, websocket connection, feature-flag fetch) get hit by parallel callers before the first call resolves, so the synchronous once would let the work run twice. Caching the in-flight Promise makes every concurrent caller share the same single fetch. The catch branch resets pending to null on failure so the next caller can retry instead of being stuck with a rejected Promise forever. This is the only correct shape for lazy bootstrap functions in modern apps; without the catch reset, one transient failure poisons the cache for the lifetime of the module.
function onceWithReset(fn) {
let called = false;
let result;
function wrapper(...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
}
wrapper.reset = () => {
called = false;
result = undefined;
};
return wrapper;
}
let seed = 0;
const getNext = onceWithReset(() => ++seed);
console.log(getNext()); // 1
console.log(getNext()); // 1
getNext.reset();
console.log(getNext()); // 2Tests, hot-module-reload, and feature-flag flips all want to clear the memoised result so the next call runs fresh. Exposing a reset method on the wrapper keeps the API ergonomic without forcing the caller to recreate the wrapper. The trade-off is that reset is now part of the public surface, so any caller can clear the cache at any time. Reach for this version inside test suites or anywhere a dependency injection layer needs to swap out the cached value (for example clearing a memoised tenant config when the tenant changes).
