Lodash 0/8

Backend engineeringUtility functionsLodash 4.18

Lodash
Toolbox

Lodash is the utility drawer of JavaScript: small, sharp tools for grouping, reading, merging and timing, each tested on the edge cases you would forget. Here are the ones you reach for daily, runnable in the page, plus which ones modern JavaScript now does for free.

JavaScript@types/lodash
00

Key terms in plain words

Never written code before? Start here. Every word below shows up later on this page, and each one comes with an everyday comparison. Underlined words in the modules link back to these cards.

28 terms in 6 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.

The drawer and what goes in it

Lodash is a kitchen utility drawer. Before using the tools, it helps to know the ingredients they work on.

Lodash
A free library of small ready-made tools for JavaScript. A library is code someone else wrote and tested that you add to your project, so you don't write the same loops by hand every time.
For example _.chunk(list, 100) is one Lodash tool. The _ is how you reach the drawer.
Function
A named set of steps you can run whenever you like. You pass values in brackets and it gives a result back. Every tool in Lodash is a function.
For example _.uniq([3, 1, 3]) runs the uniq function and gives [3, 1].
Argument
A value you pass into a function inside its brackets. The first argument is usually the data, and the ones after it say how to treat it.
For example In _.chunk(list, 2) the arguments are list and 2.
Array
A list of values kept in order, written in square brackets. Each item has a position that starts at 0.
For example ["Delhi", "Pune", "Goa"] is an array of three cities.
Object
A bundle of named values, written in curly brackets. One order or one user is usually an object, and a list of them is an array of objects.
For example { city: "Pune", total: 450 } is an object.
Key
The name of one value inside an object. Many Lodash tools take a key name to say which part of each record to look at.
For example In { city: "Pune" } the key is city.
Primitive
A simple value that is not a container: a number, a piece of text, true or false, null or undefined. Two equal primitives always count as the same, which is not true of objects.

How the tools are used

The two habits the page says make Lodash click: telling a tool what to do with each item, and never spoiling the original.

Collection
Lodash's word for anything you can walk through item by item: an array, or an object whose values you visit in turn.
Callback
A function you hand to another function so it can run yours at the right moment, often once per item.
For example In _.filter(list, (o) => o.paid), the arrow part is the callback.
Iteratee
Lodash's name for the function it runs on every item in a collection. It decides what each item becomes, or whether it stays.
Shorthand
Instead of writing a callback, you can pass just a key name, a small pattern object or a key and value pair, and Lodash builds the function for you.
For example _.map(orders, "city") reads the city of every order.
Mutate
To change a value in place, so everyone holding it sees the change. Most Lodash tools don't: they hand back something new. A few, such as merge, set and remove, do, and the page flags them.

Sorting lists and records

The knives and sorting trays: cutting lists up, tidying them and filing records where you can find them again.

Falsy
Values that count as no when a program asks yes or no: false, 0, empty text, null, undefined and NaN. _.compact throws all of them out of a list.
Removing duplicates
Keeping only one of each item. _.uniq does it for plain values. _.uniqBy does it for objects by comparing one key, which a native Set can't do.
For example _.uniqBy(users, "id") keeps one user per id.
Nested data
Objects or arrays sitting inside other objects or arrays. Real API data is often several levels deep, which is where flattening, safe reading and deep copies come in.
Lookup table
An object where each record is filed under its id, so you can go straight to it instead of searching the whole list. _.keyBy builds one from an array.
For example _.keyBy(users, "id")[7] gives user 7 at once.

Reading, copying and comparing

Reaching into the back of a cupboard safely, making a true second copy and checking two dishes really match.

Path
A route to a value deep inside nested data, written with dots or as a list of steps. _.get follows one and gives a fallback instead of crashing when a step is missing.
For example "data.user.city" is a path.
Merge
Combining two objects into one. A deep merge goes inside nested objects too, so changing one setting keeps its neighbours. Lodash's merge changes the first object you pass, which is why examples pass an empty {} first.
Deep copy
A copy where every inner box is copied too, so changing the copy can never touch the original. Spread copies only the top level, so the inner boxes stay shared.
For example _.cloneDeep(config) makes a deep copy.
Deep equality
Checking that two objects hold the same contents all the way down. === only asks whether they are the very same object, so two identical copies give false. _.isEqual compares the contents.

Kitchen timers

Tools that decide when a function runs and how often, so a busy page doesn't swamp the server.

Debounce
Waits until calls stop for a set time, then runs once with the last one. A search box uses it so it searches once you stop typing, not on every key.
Throttle
Lets a function run at most once in each window of time, and ignores the rest. Good for scroll handlers and anything that fires many times a second.
Memoize
Remembers what a function returned for each input. The next call with the same input gets the saved answer at once. It only suits a pure function, one that always gives the same output for the same input and changes nothing else.

Pipelines and shipping it

Joining tools into one line of work, and only packing the tools you really use.

Pipeline (chain and flow)
Several steps joined so the output of one feeds the next. _.chain puts your data on the belt and runs it when you ask for .value(). _.flow builds the belt with no data on it, as a function you can reuse.
Curried
A curried function can take its inputs one at a time. Give it the settings now and you get back a new function waiting for the data. That's what lets lodash/fp steps slot together.
Native
Features JavaScript has built in, with nothing to install. Many old Lodash favourites, such as flattening arrays or grouping, are now native, so you only need Lodash for what is still missing.
Bundler and dependency
A dependency is outside code your project relies on, such as Lodash. A bundler packs your code and its dependencies into the files a browser downloads. Importing one Lodash function at a time lets it leave the rest out.
Prototype pollution
A security bug where crafted input changes the hidden template every object in a program shares, so the change leaks everywhere. Older Lodash versions had it in deep merge, so stay up to date and never merge raw user input into settings.
01

The mental model

Lodash is the utility drawer in a busy kitchen: small, sharp tools that each do one job, so you stop writing the same loop by hand.

Analogy firstIterateesImmutabilityMust know

Every backend grows the same helpers: group these rows by city, read a deeply nested field without crashing, wait until the user stops typing. Lodash is the drawer where those tools already live, tested on every edge case you would forget, such as sparse arrays, null inputs and objects that pretend to be arrays.

Two habits make it click. First, iteratee shorthands: most functions accept a key name instead of a callback, so _.map(orders, "city") reads the city of each order and _.filter(orders, { paid: true }) keeps the paid ones. Second, lodash is non mutating by default: it hands you a new array or object and leaves the input alone, except for the few functions whose names say they change things, such as merge, set, assign and remove.

TermMeansExample
CollectionAn array or an object you walk throughorders, { a: 1, b: 2 }
IterateeThe function run on each item(o) => o.total
Property shorthandA key name instead of a function"total"
Matches shorthandAn object pattern to match{ paid: true }
Pair shorthandA key and a value to match["city", "Pune"]
PathA dotted or array route into nested data"data.user.city"
model.jsJS
const orders = [
  { id: 1, city: "Delhi", total: 1200, paid: true },
  { id: 2, city: "Pune", total: 450, paid: false },
  { id: 3, city: "Delhi", total: 800, paid: true },
];

// Iteratee shorthands: a string means "read this key"
console.log(_.map(orders, "city"));
console.log(_.filter(orders, { paid: true }).length);
console.log(_.sumBy(orders, "total"));
console.log(_.find(orders, ["city", "Pune"]).id);

// Lodash never changes the input unless the name says so
console.log(orders.length, orders[0].city);
TerminalOutput
$ node model.js
[ 'Delhi', 'Pune', 'Delhi' ]
2
2450
2
3 Delhi

Why it matters: shorthands turn four loops into four lines, and the last line proves the input array was not touched.

02

Arrays

Split, dedupe, compare and flatten lists without writing index maths.

chunkuniqBydifferenceflatten

Array helpers are the knives of the drawer. _.chunk cuts a list into batches, perfect for sending 500 rows to an API that accepts 100 at a time. _.uniqBy removes duplicates by a key, which native Set cannot do for objects. _.difference and _.intersection answer "what is missing" and "what is shared", such as permissions a user has lost.

_.chunk(array, size)

Splits into groups of size. The last group may be shorter.

See the example
New arrayBatching
_.compact(array)

Drops falsy values: false, null, 0, "", undefined, NaN.

See the example
New arrayClean up
_.uniqBy(array, iteratee)

Keeps the first item for each key.

See the example
New arrayDedupe objects
_.difference(array, values)

Items in the first array but not in the second.

See the example
New arraySet logic
_.flattenDeep(array)

Flattens every level of nesting.

See the example
New arrayNative: flat(Infinity)
_.zip(...arrays)

Pairs items by index, like a zipper.

See the example
New arrayTranspose
arrays.jsJS
console.log(_.chunk([1, 2, 3, 4, 5], 2));
console.log(_.compact([0, "a", "", null, "b", false, undefined]));
console.log(_.uniq([3, 1, 3, 2, 1]));
console.log(_.uniqBy([{ id: 1 }, { id: 2 }, { id: 1 }], "id"));
console.log(_.difference(["read", "write", "admin"], ["admin"]));
console.log(_.intersection(["node", "go", "rust"], ["rust", "node"]));
console.log(_.flattenDeep([1, [2, [3, [4]]]]));
console.log(_.zip(["a", "b"], [1, 2]));
console.log(_.take([10, 20, 30, 40], 2), _.last([10, 20, 30]));
TerminalOutput
$ node arrays.js
[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]
[ 'a', 'b' ]
[ 3, 1, 2 ]
[ { id: 1 }, { id: 2 } ]
[ 'read', 'write' ]
[ 'node', 'rust' ]
[ 1, 2, 3, 4 ]
[ [ 'a', 1 ], [ 'b', 2 ] ]
[ 10, 20 ] 30

Why it matters: each call answers one question in one line, and every result is a fresh array, so the originals can be reused safely.

FunctionDoesNative close match
_.chunk(xs, n)Batches of nNone built in
_.uniq(xs)Unique primitives[...new Set(xs)]
_.uniqBy(xs, "id")Unique by keyNone built in
_.flatten / _.flattenDeepOne level / all levelsxs.flat() / xs.flat(Infinity)
_.head / _.lastFirst / last itemxs[0] / xs.at(-1)
_.take(xs, n)First n itemsxs.slice(0, n)
_.sortedUniq, _.pull, _.removeSorted dedupe, remove in placepull and remove mutate
03

Collections

Group, index, count, sort and split lists of records, the daily bread of report and API code.

groupBykeyByorderByMust know

Collection helpers are the sorting trays. _.groupBy drops each record into a tray by a key and gives you arrays, while _.keyBy gives you exactly one record per key, which turns an array of users into a lookup table by id. _.countBy only counts, and _.orderBy sorts by several keys with a direction for each, something native sort makes you write by hand.

_.groupBy(coll, key)

Key to array of items. Many per key.

See the example
ObjectReports
_.keyBy(coll, key)

Key to one item. The last one wins.

See the example
ObjectLookups by id
_.countBy(coll, key)

Key to how many items share it.

See the example
ObjectStats
_.orderBy(coll, keys, orders)

Multi key sort with asc or desc per key.

See the example
New arrayStable
_.partition(coll, fn)

Two arrays: items that pass and items that fail.

See the example
Two arraysSplit
_.sumBy / _.maxBy(coll, key)

Totals and extremes by a key.

See the example
One valueAggregates
collections.jsJS
const users = [
  { name: "Asha", team: "api", age: 29 },
  { name: "Ravi", team: "web", age: 34 },
  { name: "Meera", team: "api", age: 25 },
];

console.log(_.groupBy(users, "team").api.length);
console.log(_.keyBy(users, "name").Ravi.team);
console.log(_.countBy(users, "team"));
console.log(_.map(_.orderBy(users, ["team", "age"], ["asc", "desc"]), "name"));
console.log(_.partition(users, (u) => u.age >= 30).map((g) => g.length));
console.log(_.maxBy(users, "age").name, _.round(_.meanBy(users, "age"), 1));
TerminalOutput
$ node collections.js
2
web
{ api: 2, web: 1 }
[ 'Asha', 'Meera', 'Ravi' ]
[ 1, 2 ]
Ravi 29.3

Why it matters: keyBy turns a search through an array into a direct property read, and orderBy sorts by team ascending then age descending in one call.

You haveYou wantWrite
Rows with a cityRows per city_.groupBy(rows, "city")
Users with idsFind by id fast_.keyBy(users, "id")
Events with a typeCount per type_.countBy(events, "type")
OrdersNewest first, then biggest_.orderBy(o, ["at", "total"], ["desc", "desc"])
JobsDone and not done_.partition(jobs, "done")
04

Objects

Read deep paths safely, pick and drop keys, merge configs and compare or copy nested data.

getmergecloneDeepMust know

Objects are the containers in the drawer. _.get is a torch for dark cupboards: it walks a path and returns a default instead of throwing when a shelf is missing. _.pick and _.omit shape a response, such as removing a password hash before sending a user out.

_.merge blends configs deeply, so overriding db.pool keeps db.host. It mutates its first argument, which is why the example passes a fresh {} first. _.cloneDeep makes a fully separate copy and _.isEqual compares by value, the two things === and spread cannot do for nested data.

objects.jsJS
const res = { data: { user: { profile: { city: "Delhi" }, tags: ["a"] } } };

console.log(_.get(res, "data.user.profile.city"));
console.log(_.get(res, "data.user.phone", "not set"));
console.log(_.has(res, ["data", "user", "tags", 0]));

const cfg = { port: 3000, db: { host: "localhost", pool: 5 } };
console.log(_.pick(cfg, ["port"]), _.omit(cfg, ["db"]));
console.log(_.merge({}, cfg, { db: { pool: 20 } }));
console.log(_.defaults({ port: 8080 }, cfg).port);

const copy = _.cloneDeep(cfg);
copy.db.pool = 99;
console.log(cfg.db.pool, _.isEqual(cfg, _.cloneDeep(cfg)));
TerminalOutput
$ node objects.js
Delhi
not set
true
{ port: 3000 } { port: 3000 }
{ port: 3000, db: { host: 'localhost', pool: 20 } }
8080
5 true

Why it matters: changing the deep copy left the original pool at 5, and isEqual says two separate objects with the same contents are equal.

FunctionMutates inputUse for
_.get(obj, path, fallback)NoReading optional nested data
_.set(obj, path, value)YesWriting a deep path, creating levels
_.pick / _.omitNoShaping responses
_.merge(target, ...src)Yes, targetDeep config layering
_.defaults(target, ...src)Yes, targetFill only missing keys
_.mapValues(obj, fn)NoTransform every value
_.cloneDeep(obj)NoIndependent copies
_.isEqual(a, b)NoDeep equality checks
05

Function helpers

Control when and how often functions run: debounce, throttle, memoize and once.

debouncethrottlememoizeMust know

These are the kitchen timers. _.debounce waits for quiet: every call resets the timer, so a search box fires once after the user stops typing. _.throttle is a turnstile: calls pass at most once per window, so a scroll or metrics handler cannot flood the server.

_.memoize keeps a notebook of answers keyed by the first argument, so a slow pure function runs once per input. _.once is a fuse that burns once: perfect for opening a single database connection however many modules ask for it.

Debounce_.debounce(fn, 300)Runs once after 300 ms of silence.
Throttle_.throttle(fn, 300)Runs at most once every 300 ms.
timing.jsJS
// debounce: wait until the typing stops, then run once
const search = _.debounce((q) => console.log("search:", q), 100);
search("z"); search("zo"); search("zod");

// throttle: run at most once per window, however often it fires
let calls = 0;
const onScroll = _.throttle(() => calls++, 100);
for (let i = 0; i < 50; i++) onScroll();
setTimeout(() => console.log("throttled calls:", calls), 300);

// memoize: cache by the first argument
const slowSquare = _.memoize((n) => { console.log("computing", n); return n * n; });
console.log(slowSquare(9), slowSquare(9));

// once: the second call returns the first result
const init = _.once(() => { console.log("connecting"); return "db"; });
console.log(init(), init());
TerminalOutput
$ node timing.js
computing 9
81 81
connecting
db db
search: zod
throttled calls: 2

Why it matters: three rapid searches become one call with the final query, and fifty scroll events become two: the leading call and one trailing call. The memoized square computes only once.

HelperUseful optionsWatch out for
_.debounce(fn, ms, opts)leading, trailing, maxWaitCall .cancel() on unmount or shutdown
_.throttle(fn, ms, opts)leading, trailingBoth edges fire by default
_.memoize(fn, resolver)Custom cache key via resolverCache grows forever; clear fn.cache
_.once(fn)NoneErrors are not retried
06

Strings and utilities

Convert naming styles, trim text for display, and reach for the small number and type helpers.

camelCasetruncaterangeisEmpty

String helpers are the label maker. Backend code constantly crosses naming styles: database columns in snake_case, JSON in camelCase, URLs in kebab-case. Lodash converts between them from any starting style, and _.escape makes text safe to drop into HTML.

The utility helpers fill small gaps: _.range and _.times build sequences, _.clamp keeps a page size inside limits, and _.isEmpty answers "is there anything here" for objects, arrays, maps and strings alike.

strings.jsJS
console.log(_.camelCase("order created at"));
console.log(_.kebabCase("OrderCreatedAt"));
console.log(_.snakeCase("orderCreatedAt"));
console.log(_.startCase("user_first_name"));
console.log(_.truncate("Zod and Joi walk into a bar", { length: 16 }));
console.log(_.padStart("7", 3, "0"), _.escape("<b>hi</b>"));

console.log(_.range(0, 20, 5), _.times(3, (i) => i * 2));
console.log(_.clamp(150, 0, 100), _.inRange(3, 1, 5));
console.log(_.isEmpty({}), _.isEmpty([0]), _.isNil(null));
TerminalOutput
$ node strings.js
orderCreatedAt
order-created-at
order_created_at
User First Name
Zod and Joi w...
007 &lt;b&gt;hi&lt;/b&gt;
[ 0, 5, 10, 15 ] [ 0, 2, 4 ]
100 true
true false true

Why it matters: isEmpty([0]) is false because the array has one item, even though that item is falsy. clamp is a tidy guard for a limit query parameter.

InputFunctionOutput
"order created at"_.camelCaseorderCreatedAt
"OrderCreatedAt"_.kebabCaseorder-created-at
"orderCreatedAt"_.snakeCaseorder_created_at
"user_first_name"_.startCaseUser First Name
"hello"_.upperFirstHello
"<b>"_.escape&lt;b&gt;
07

Chaining and flow

Compose several steps into one readable pipeline, either wrapped with chain or as a reusable function with flow.

chainflowlodash/fp

_.chain(value) puts your data on a conveyor belt. Each step reads like a sentence, group, map, order, take, and nothing leaves the belt until .value(). _.flow builds the belt without the data, giving you a function you can name, test and reuse.

There is a catch for bundles: chain pulls in the whole library, because any method could appear on the wrapper. If size matters, prefer flow with individually imported functions, or lodash/fp, a variant where every function takes the data last and is curried, so steps compose without wrappers.

pipeline.jsJS
const sales = [
  { city: "Delhi", amount: 1200 },
  { city: "Pune", amount: 450 },
  { city: "Delhi", amount: 800 },
  { city: "Goa", amount: 300 },
];

// chain wraps a value; nothing runs until .value()
const top = _.chain(sales)
  .groupBy("city")
  .map((rows, city) => ({ city, total: _.sumBy(rows, "amount") }))
  .orderBy("total", "desc")
  .take(2)
  .value();
console.log(top);

// flow builds the same pipeline as a reusable function
const cityNames = _.flow([(xs) => _.map(xs, "city"), _.uniq, _.sortBy]);
console.log(cityNames(sales));
TerminalOutput
$ node pipeline.js
[ { city: 'Delhi', total: 2000 }, { city: 'Pune', total: 450 } ]
[ 'Delhi', 'Goa', 'Pune' ]

Why it matters: the chain reads top to bottom like a report spec: total sales per city, highest first, top two. The same idea as flow becomes a named function you can drop into any handler.

08

Native vs lodash

Modern JavaScript absorbed many lodash classics. Know which ones, and import only what you still need.

Native firstTree shakingSecurityMust know

The language has caught up on a lot of the drawer: Object.groupBy, flat, Set, optional chaining with ??, structuredClone, at(-1) and toSorted now cover common cases with no dependency. What native still lacks is deep equality, a deep merge, debounce and throttle, the case converters and the multi key sort, which is where lodash keeps earning its place.

Import with care. import _ from "lodash" brings in the whole library; per method imports such as import debounce from "lodash/debounce", or the ES module build lodash-es, let a bundler keep only what you use. Older 4.17 releases had prototype pollution bugs in deep setters such as merge and set, so stay on the latest release and never deep merge a raw request body into config.

native.jsJS
const users = [{ name: "Asha", team: "api" }, { name: "Ravi", team: "web" }];

// Native now covers many lodash classics
console.log(Object.keys(Object.groupBy(users, (u) => u.team)));
console.log([1, [2, [3]]].flat(Infinity), [...new Set([1, 1, 2])]);
console.log(users[0]?.profile?.city ?? "not set");
const copy = structuredClone({ a: { b: 1 } });
console.log(copy.a.b);

// But native has no deep equality or safe deep merge
console.log({ a: 1 } === { a: 1 }, _.isEqual({ a: 1 }, { a: 1 }));
const merged = { ...{ db: { host: "x", pool: 5 } }, ...{ db: { pool: 20 } } };
console.log(merged.db, _.merge({ db: { host: "x", pool: 5 } }, { db: { pool: 20 } }).db);
TerminalOutput
$ node native.js
[ 'api', 'web' ]
[ 1, 2, 3 ] [ 1, 2 ]
not set
1
false true
{ pool: 20 } { host: 'x', pool: 20 }

Why it matters: spread only merges the top level, so db.host vanished, while _.merge kept it. Two identical object literals are never ===, which is why isEqual still matters.

LodashNative todayKeep lodash when
_.groupByObject.groupBy(xs, fn)You need key shorthands
_.flattenDeepxs.flat(Infinity)Never, native is fine
_.uniq[...new Set(xs)]Never for primitives
_.getobj?.a?.b ?? fallbackThe path is dynamic, like "a.b.c"
_.cloneDeepstructuredClone(obj)You clone functions or class instances
_.lastxs.at(-1)Never
_.sortByxs.toSorted(cmp)You sort by several keys
_.isEqualNoneAlways
_.mergeSpread is shallow onlyAlways, for deep config
_.debounce / _.throttleNoneAlways
Go

Which one do I need?

Find your situation, take the tool in the middle column and copy the starting line.

SituationReach forStart with
Batch rows for a bulk APIArrays_.chunk(rows, 100)
Report totals per groupCollections_.groupBy then _.sumBy
Look records up by idCollections_.keyBy(users, "id")
Read an optional nested fieldObjects_.get(res, "data.user.city", null)
Layer config filesObjects_.merge({}, base, env, local)
Compare two payloadsObjects_.isEqual(a, b)
Search as the user typesFunctions_.debounce(search, 300)
Rate limit a noisy handlerFunctions_.throttle(send, 1000)
snake_case columns to JSONStrings_.mapKeys(row, (v, k) => _.camelCase(k))
Flatten, dedupe, clone simplyNative firstflat(), new Set(), structuredClone()