DataLoader
DataLoader is a JavaScript utility designed to optimize data fetching from various backend sources like databases or web services. It achieves this by intelligently batching multiple individual data requests into a single operation and caching results, significantly reducing the number of round-trips to the backend. The current stable version is 2.2.3, with minor patch releases occurring regularly to address fixes and small improvements. Major versions, like v2.0.0, introduce breaking changes and significant architectural updates, such as becoming part of the GraphQL Foundation. A key differentiator is its core focus on solving the N+1 problem by providing a simple, consistent API that coalesces requests within a single event loop tick, making it particularly valuable in GraphQL server implementations where diverse data requirements are common. It ships with TypeScript types, ensuring robust development in typed environments and is generally released on an as-needed basis rather than a fixed cadence.
Common errors
-
TypeError: DataLoader is not a constructor
cause Attempting to use `new DataLoader()` after importing with `import DataLoader from 'dataloader';` (a default import) when `dataloader` primarily exports a named `DataLoader` class in ESM, or an incorrect `require` statement.fixUse the correct named import for ESM: `import { DataLoader } from 'dataloader';`. For CommonJS, use `const DataLoader = require('dataloader');` to ensure you're getting the constructor. -
Invariant Violation: DataLoader must be constructed with a function which accepts an Array of keys and returns a Promise which resolves to an Array of the same length as the Array of keys.
cause The batch loading function passed to the `DataLoader` constructor returned an array with a different number of elements than the input `keys` array, or did not return a Promise resolving to such an array.fixReview your batch loading function to ensure it always returns a Promise that resolves to an array whose length is exactly equal to the `keys` array it received, and that each element corresponds to the respective key in order (either a value or an `Error` object).
Warnings
- breaking As of v2.0.0, the `.loadMany()` method now returns an Array which may contain `Error` objects for individual keys that failed, rather than throwing a single error for the entire batch. Consumers must now explicitly check for `Error` instances within the returned array.
- gotcha DataLoader instances typically represent a unique cache and context. In web servers (e.g., Express, GraphQL), a new DataLoader instance should be created per-request to prevent data leaks between different users and ensure appropriate caching semantics for mutable data. Do not use a single global DataLoader instance across all requests.
- gotcha DataLoader's batching mechanism coalesces individual `load()` calls that occur within a single 'tick' of the JavaScript event loop. If you `await` a `load()` call before making another, the second `load()` call will not be batched with the first, potentially leading to N+1 queries. Structure your code to defer `await` until all desired `load()` calls have been queued, often by using `Promise.all()`.
- gotcha The batch loading function provided to DataLoader *must* return a Promise that resolves to an Array of values (or Errors) that is exactly the same length as the input `keys` Array, and in the *same order* as the input keys. Failing to adhere to this contract will result in an `Invariant Violation` error.
- gotcha When using complex objects as keys for `loader.load()`, JavaScript's default Map behavior uses reference equality. If you pass different object instances with the same logical content, they will be treated as distinct keys, bypassing the cache. To address this, provide a custom `cacheKeyFn`.
- deprecated While DataLoader gained direct browser support in v1.4.0, it explicitly notes that it "cannot rely on the same post-promise job queuing behavior that allows for best performance in Node environments." This implies a fallback behavior is used, and performance characteristics in the browser might differ.
Install
-
npm install dataloader -
yarn add dataloader -
pnpm add dataloader
Imports
- DataLoader
const DataLoader = require('dataloader');import { DataLoader } from 'dataloader'; - BatchLoadFn
import type { BatchLoadFn } from 'dataloader'; - DataLoader
const DataLoader = require('dataloader');
Quickstart
import { DataLoader } from 'dataloader';
interface User { id: number; name: string; invitedByID?: number; }
// Simulate a backend API call that fetches users by a batch of IDs
async function myBatchGetUsers(ids: readonly number[]): Promise<(User | Error)[]> {
console.log(`[DB] Fetching users with IDs: ${ids.join(', ')}`);
// In a real app, this would be a single database query or API call
const users: User[] = ids.map(id => ({
id,
name: `User ${id}`,
invitedByID: id > 1 ? id - 1 : undefined // Example for chaining loads
}));
// Return in the same order as requested keys
return ids.map(id => users.find(u => u.id === id) || new Error(`User ${id} not found`));
}
async function main() {
// Create a new DataLoader instance per request (or execution context)
const userLoader = new DataLoader<number, User>(myBatchGetUsers, {
cache: true, // Caching is enabled by default, explicitly shown for clarity
cacheKeyFn: (key) => `user:${key}` // Custom cache key for clarity, default works for primitives
});
console.log('--- Initiating parallel loads (will be batched) ---');
const [user1, user2] = await Promise.all([
userLoader.load(1),
userLoader.load(2)
]);
if (user1 instanceof Error || user2 instanceof Error) {
console.error('Error loading users:', user1, user2);
return;
}
console.log(`Loaded User 1: ${user1.name}`);
console.log(`Loaded User 2: ${user2.name}`);
console.log('\n--- Chaining loads (will be batched if in same tick) ---');
const invitedBy1 = await userLoader.load(user1.invitedByID || 0); // Assuming user1 has invitedByID
const invitedBy2 = await userLoader.load(user2.invitedByID || 0); // Assuming user2 has invitedByID
if (invitedBy1 instanceof Error || invitedBy2 instanceof Error) {
console.error('Error loading invitedBy users:', invitedBy1, invitedBy2);
return;
}
console.log(`User ${user1.id} was invited by ${invitedBy1.name}`);
console.log(`User ${user2.id} was invited by ${invitedBy2.name}`);
console.log('\n--- Loading cached value (no DB call) ---');
const cachedUser1 = await userLoader.load(1);
console.log(`Loaded cached User 1: ${cachedUser1 instanceof Error ? 'Error' : cachedUser1.name}`);
}
main().catch(console.error);