Object.groupBy() and Map.groupBy(): The JavaScript Built-Ins That Replace lodash _.groupBy
The lodash _.groupBy function has been the most-imported single function in the entire JavaScript ecosystem for years. Every codebase that does any kind of data processing reaches for it eventually, because grouping a list of items by a derived key is one of the three or four operations you do most often when transforming data. ES2024 added Object.groupBy and Map.groupBy as standard built-ins, which means you can finally drop the lodash dependency in most projects without giving up the convenience. This article walks through what they do, when to pick which, and the gotchas that aren’t obvious from the spec.
The function the language was missing
The pattern is so common that every codebase eventually writes it inline if they don’t have a library. The hand-rolled version with reduce:
function groupBy(items, keyFn) {
return items.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] ||= []).push(item);
return acc;
}, {});
}
Eight lines, and you have to remember to use ||= for the empty-array initialization, and you have to remember to return acc, and the reduce starting value has to be exactly the right shape. It’s the kind of function that’s easy to write wrong on the first try and easy to forget exists if you don’t know what to grep for. Putting it in the standard library is a small ergonomic win that compounds across millions of codebases.

Object.groupBy vs Map.groupBy
The new feature is actually two methods. Object.groupBy returns a plain object where the keys are the grouping criterion stringified. Map.groupBy returns a Map where the keys are the grouping criterion as-is — which means you can group by object identity, not just by string key.
const items = [
{ type: 'fruit', name: 'apple' },
{ type: 'fruit', name: 'banana' },
{ type: 'vegetable', name: 'carrot' },
];
// Plain object — keys are strings
const byType = Object.groupBy(items, item => item.type);
// { fruit: [...], vegetable: [...] }
// Map — keys can be anything
const byTypeMap = Map.groupBy(items, item => item.type);
// Map(2) { 'fruit' => [...], 'vegetable' => [...] }
For the simple case where the grouping key is a string, both work. The Object form is slightly more ergonomic because plain object access (byType.fruit) is shorter than Map.get (byTypeMap.get('fruit')) and it serializes to JSON without ceremony. The Map form is the right choice when:
- You’re grouping by an object reference rather than a string. Example: grouping orders by their customer object instead of by customer ID.
- You need to preserve insertion order of keys. Plain objects technically preserve insertion order in modern engines, but the spec is murky for non-string keys; Map is unambiguous.
- You’ll be iterating the result later. Map iteration is a few percent faster than Object.keys + access.
For the 90% case, Object.groupBy is what you want.
Real run with the output
Here’s the actual output from a docker run on node:22-slim, grouping a list of food items by their type:

Notice that both forms produce the same logical grouping — three fruit items, two vegetables, one grain — but the Object form is a JSON-friendly nested structure and the Map form is iterable with the for-of pattern. The Object form is what you’d shove into an API response; the Map form is what you’d consume in a downstream pipeline.
Where it differs from lodash _.groupBy
Most lodash _.groupBy calls translate to Object.groupBy with one syntactic change: lodash accepts either a function or a property name string as the second argument; the standard built-in only accepts a function.
// lodash
_.groupBy(items, 'type');
_.groupBy(items, item => item.type);
// standard
Object.groupBy(items, item => item.type);
// 'type' as a string does not work — you must pass a function
The function-only requirement is a deliberate design choice. The TC39 committee preferred consistency with map and filter (which also take functions) over the lodash convention of accepting either. The downside is that the very common pattern of “group by a property name” is two characters longer in standard JavaScript than in lodash. The upside is that the standard form supports any computation as the grouping criterion, not just property access.
The other small difference: lodash converts non-string return values from the key function to strings before grouping (so grouping by a number works the same way it works in JS object literals). Object.groupBy does the same — non-string keys are coerced to string. Map.groupBy doesn’t coerce, so you can group by numbers, booleans, or objects without losing the type.
When you should still use lodash _.groupBy
To be honest about the migration: if you already have lodash in your dependency tree for other reasons, swapping out _.groupBy for Object.groupBy is busywork that doesn’t help anything. The bundle size win from removing one lodash function is negligible if you’re keeping _.debounce and _.cloneDeep and a dozen other things from the same library.
The migration matters in two cases:
- Your codebase imports lodash only for groupBy. Now you can drop the dependency entirely and shave 70KB off the bundle.
- You’re starting a new project and trying to avoid lodash from day one. Use the built-in for groupBy and reach for individual lodash-es subpackages (or pure native equivalents) for anything else you need.
For greenfield projects in 2026, the case for lodash as a default dependency is weaker than it used to be. Almost everything lodash does has a native equivalent now: groupBy, structuredClone for cloneDeep, Array.prototype.with for immutable updates, the Set/Map difference and intersection methods. The exception is _.debounce and _.throttle, which the language still doesn’t have natively and which most projects re-implement badly when they don’t import lodash.
Browser and runtime support
Both methods shipped in 2024:
- Chrome 117 (September 2023)
- Firefox 119 (October 2023)
- Safari 17.4 (March 2024)
- Node.js 21+ (the polyfill landed in v8 ahead of the official release)
If you need to support older runtimes, the polyfill is straightforward:
if (!Object.groupBy) {
Object.groupBy = function(items, keyFn) {
return items.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] ||= []).push(item);
return acc;
}, Object.create(null));
};
}
The Object.create(null) instead of {} is a small detail — it gives you an object with no prototype, which avoids the rare bug where someone tries to group by the key “toString” or “hasOwnProperty” and gets a collision with the inherited method.
Composition with map, filter, reduce
The natural place groupBy lives is in a chain with the other array methods. A common shape for analytics or reporting code:
const summary = Object.entries(
Object.groupBy(orders.filter(o => o.status === 'paid'), o => o.customerId)
).map(([customerId, customerOrders]) => ({
customerId,
count: customerOrders.length,
total: customerOrders.reduce((sum, o) => sum + o.amount, 0),
}));
Filter to just paid orders, group by customer, then for each group compute count and total. Five lines of standard JavaScript that used to need a library or a verbose hand-rolled loop. This is the kind of code that earns its place in your data layer.
The bottom line
Object.groupBy and Map.groupBy are small additions to the language that remove a long-running source of friction. Use Object.groupBy for the common case of grouping by a string key into a JSON-friendly result. Use Map.groupBy when you’re grouping by a non-string key or when you’re consuming the result in an iteration pipeline. Drop lodash _.groupBy from any project where it was your only reason for the dependency, and remember that the function-only signature means you can’t pass a property name string the way lodash let you.
