Skip to content

memoize

memoize creates a cached async function. Calls with the same cache key return a CancellableHandle that resolves to the cached value after the first successful execution stores it.

Best for cached async lookups Use memoize for id-based fetches, configuration lookups, permission checks, or other async reads where repeated calls should reuse prior results.

Root package:

import { memoize } from "@vgerbot/async";

Module subpath:

import { memoize } from "@vgerbot/async/utils";

Leaf subpath:

import { memoize } from "@vgerbot/async/utils/memoize";
import { memoize } from "@vgerbot/async";
const getUser = memoize(async (id: number, token) => {
const response = await token.wrap(fetch(`/api/users/${id}`));
return response.json() as Promise<{ id: number; name: string }>;
});
const first = getUser(1);
const user = await first.promise;
const second = getUser(1);
const cachedUser = await second.promise;
console.log(user === cachedUser);
  • Repeated reads: The same arguments should reuse a previous result.
  • Cancellable callers: Each call should still return a CancellableHandle.
  • Simple cache control: Consumers can inspect or clear memoized.cache.
  • Custom keys: Multiple arguments need a stable custom cache key.
function memoize<TArgs extends unknown[], TResult>(
fn: (...args: [...TArgs, CancellableToken]) => Promise<TResult>,
options?: MemoizeOptions,
): ((...args: TArgs) => CancellableHandle<TResult>) & {
cache: Map<string, TResult>;
};

The async function to cache. memoize passes the original call arguments followed by a CancellableToken.

const getUser = memoize(async (id: number, token) => {
await token.sleep(100);
return { id };
});

MemoizeOptions extends shared CancellableOptions and adds resolver.

Option Type Default Description
resolver (...args: unknown[]) => string String(args[0]) Creates the cache key for a call.
name string undefined Optional name passed to generated cancellable handles.
signal AbortSignal undefined External signal linked to each generated handle.
timeout number undefined Timeout applied to each generated handle.
fallback value or function undefined Fallback used when a generated handle rejects.
retry RetryOptions undefined Retry configuration for uncached executions.
onCancel function undefined Called when a generated handle is cancelled.
onRetry function undefined Called before a retry attempt.

memoize returns a function with the same public arguments as fn, excluding the injected CancellableToken. The returned function also has a cache map.

const getUser = memoize(fetchUser);
const handle = getUser(1);
const user = await handle.promise;
getUser.cache.clear();

The default resolver uses the first argument converted to a string.

const byId = memoize(async (id: number, token) => {
await token.sleep(100);
return { id };
});
await byId(1).promise;
console.log(byId.cache.has("1"));

Use a custom resolver when multiple arguments define identity.

const getRepo = memoize(
async (owner: string, repo: string, token) => {
const response = await token.wrap(fetch(`/api/repos/${owner}/${repo}`));
return response.json();
},
{
resolver: (owner, repo) => `${owner}/${repo}`,
},
);

Only successful uncached executions are stored. If fn rejects, no value is added to cache unless a shared cancellable fallback resolves a value.

Every call returns a CancellableHandle. Cancelling an uncached call cancels that execution. Cancelling a cached call cancels only the handle returned for that cached value.

const getSlowValue = memoize(async (id: number, token) => {
await token.sleep(5_000);
return id;
});
const handle = getSlowValue(1);
handle.cancel("No longer needed");
await handle.promise;

Uncached execution errors reject the returned handle and do not populate the cache.

const getConfig = memoize(async (_name: string) => {
throw new Error("Config unavailable");
});
await getConfig("app").promise.catch(() => undefined);
console.log(getConfig.cache.has("app")); // false

memoize infers the public arguments from all parameters before the trailing CancellableToken.

const loadRange = memoize(
async (start: number, end: number, token) => {
await token.sleep(100);
return Array.from({ length: end - start }, (_, index) => start + index);
},
{
resolver: (start, end) => `${start}:${end}`,
},
);
const values = await loadRange(10, 15).promise;
  • Utils lists other function wrappers.
  • Cancellable explains CancellableHandle and CancellableToken.
  • queue is better when cached work should be processed through a shared worker queue.