All questions

DataLoader

Premium

DataLoader

A DataLoader batches the individual load(key) calls made during one tick of the event loop into a single call to a batch function, then caches each key's result so repeat loads are free. It is the standard fix for the N+1 problem: instead of firing one request per key as the keys arrive, you collect a tick's worth of keys and fetch them all at once. This is the pattern popularised by Facebook's DataLoader, the backbone of most GraphQL servers.

Implement dataloader(batchFn). It returns a loader with load, loadMany, clear, and clearAll. Every load(key) returns a promise; all load calls made in the same synchronous tick are gathered and dispatched together as one batchFn(keys) on the next microtask. batchFn returns a promise of an array of values, positionally matched to the keys it was given.

Signature

function dataloader<K, V>(
  batchFn: (keys: K[]) => Promise<Array<V | Error>>
): DataLoader<K, V>;

interface DataLoader<K, V> {
  load(key: K): Promise<V>;          // joins the current tick's batch, or the cache
  loadMany(keys: K[]): Promise<V[]>; // Promise.all of loads, values in order
  clear(key: K): void;               // evict one key from the cache
  clearAll(): void;                  // evict every key
}

Examples

const users = dataloader((keys) => {
  // Called ONCE with ['1', '2', '3'] — not three times.
  return Promise.resolve(keys.map((id) => ({ id, name: 'User ' + id })));
});

// Three loads in the same tick collapse into a single batchFn call.
const [a, b, c] = await Promise.all([
  users.load('1'),
  users.load('2'),
  users.load('3'),
]);

a.name; // 'User 1'   (the value at index 0 of the batch)
c.name; // 'User 3'   (the value at index 2)
const users = dataloader((keys) => Promise.resolve(keys.map(fetchUser)));

users.load('1');
users.load('1'); // same tick, same key → batchFn sees ['1'] once; both share it

await users.load('1'); // resolved and now cached
users.load('1');       // a later tick → returns the cached promise, no new batch
users.clear('1');      // evict → the next load('1') batches afresh

Notes

  • One tick, one batch — every load called before the next microtask is collected; batchFn runs once with all the keys, in first-requested order. Use queueMicrotask (or Promise.resolve().then), not setTimeout.
  • Positional resultsbatchFn(keys) must resolve to an array where values[i] is the answer for keys[i], with the same length and order.
  • Errors are per key — if values[i] is an Error instance, only that key's load rejects; the rest still resolve. If batchFn itself rejects, every load in that batch rejects.
  • Dedupe within a batch — the same key requested twice in one tick appears once in keys; both callers share the one result.
  • Cache across ticks — a key's promise is memoised after its first load and reused on later ticks until you clear(key) or clearAll().
  • You control the keys — dedupe and cache hits happen only when callers pass the same key value, so identical work must map to the same key.

FAQ

Why batch on a microtask instead of setTimeout?
queueMicrotask dispatches the batch at the end of the current event-loop turn, before any timer or I/O callback, so awaited loads settle within the same turn. A setTimeout(0) would push the batch to a macrotask a whole turn later and let it interleave with other timers.
How does DataLoader dedupe repeated keys?
The per-key cache is populated on a key's first load, so a second load of the same key in the same tick hits the cache and never adds a duplicate to the batch. batchFn sees each key once and both callers share the one result.
What happens when the batch function returns an Error for one key?
Only that key's load rejects with the Error; the other keys in the same batch still resolve with their values. A batch-wide failure happens only when batchFn itself rejects, which rejects every pending load in that batch.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium