Yevhen Klymentiev
dark
light
console
darkness
y.klymentiev@gmail.com
Reusable Snippets|Practical utility code for everyday use — custom-built and ready to share

onceAsync

Ensures an async function is only executed once. Subsequent calls return the same Promise. Includes a .reset() method to allow re-invocation.

TypeScript
Copied!
1/**
2 * Ensures an async function is only executed once.
3 * Subsequent calls return the same Promise.
4 * Includes a .reset() method to allow re-invocation.
5 *
6 * @param fn - The async function to wrap.
7 * @returns A function that only runs once, with .reset() support.
8 */
9export function onceAsync<T extends (...args: any[]) => Promise<any>>(
10  fn: T
11): (() => ReturnType<T>) & { reset: () => void } {
12  let promise: ReturnType<T> | null = null;
13
14  const wrapper = () => {
15    if (!promise) {
16      promise = fn();
17    }
18    return promise;
19  };
20
21  wrapper.reset = () => {
22    promise = null;
23  };
24
25  return wrapper;
26}
  • Single Execution Guarantee

    Ensures the wrapped async function is only triggered once, even across concurrent invocations.

  • Promise Reuse

    Returns the same Promise for all subsequent calls, preventing duplicate work and unnecessary async operations.

  • Resettable

    Provides a .reset() method to clear cached execution, enabling controlled re-execution when needed.

  • Concurrency Safe

    Avoids race conditions by storing the in-progress promise, ensuring only one instance of the async function runs at a time.

Tests | Examples

TypeScript
Copied!
1test('onceAsync - only runs the function once', async () => {
2  const fn = jest.fn().mockResolvedValue('ok');
3  const onceFn = onceAsync(fn);
4
5  const result1 = await onceFn();
6  const result2 = await onceFn();
7
8  expect(result1).toBe('ok');
9  expect(result2).toBe('ok');
10  expect(fn).toHaveBeenCalledTimes(1);
11});
12
13test('onceAsync - multiple calls before resolve return same promise', async () => {
14  let resolver: (v: string) => void = () => {};
15  const promise = new Promise<string>(res => { resolver = res; });
16
17  const fn = jest.fn().mockReturnValue(promise);
18  const onceFn = onceAsync(fn);
19
20  const call1 = onceFn();
21  const call2 = onceFn();
22
23  resolver('resolved');
24
25  const [res1, res2] = await Promise.all([call1, call2]);
26
27  expect(res1).toBe('resolved');
28  expect(res2).toBe('resolved');
29  expect(fn).toHaveBeenCalledTimes(1);
30});
31
32test('onceAsync - reset allows function to be called again', async () => {
33  const fn = jest.fn()
34    .mockResolvedValueOnce('first')
35    .mockResolvedValueOnce('second');
36  const onceFn = onceAsync(fn);
37
38  const first = await onceFn();
39  expect(first).toBe('first');
40  expect(fn).toHaveBeenCalledTimes(1);
41
42  onceFn.reset();
43
44  const second = await onceFn();
45  expect(second).toBe('second');
46  expect(fn).toHaveBeenCalledTimes(2);
47});

Common Use Cases

  • Lazy Initialization

    Initialize resources like database connections, API clients, or WebSocket sessions only once per lifecycle.

  • Memoized Async Configuration

    Load configuration or environment data once and share it across your application.

  • Singleton Service Setup

    Run costly setup tasks (e.g. authentication bootstrap, schema loading) a single time on first use.

  • Prevent Duplicate API Calls

    Avoid redundant network requests caused by multiple parallel triggers for the same operation.

  • Testing and Mocking

    Control async mocks or stubs so they only resolve once per test run unless explicitly reset.

Codebase: Utilities -> Functions -> onceAsync | Yevhen Klymentiev