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.
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
- Think of it as the utility drawer in a busy kitchen
- 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
- Think of it as a recipe card: give it ingredients and it hands back a dish
- 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
- Think of it as the ingredients you hand over with the recipe card
- 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 arelistand2. - Array
- Think of it as a row of jars on a shelf, each in its numbered spot
- 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
- Think of it as a labelled spice box where every compartment has a name
- 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
- Think of it as the label on one compartment of the spice box
- 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 iscity. - Primitive
- Think of it as a single plain ingredient, like one egg, rather than a whole box
- 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
- Think of it as a whole tray of ingredients you work through one by one
- Lodash's word for anything you can walk through item by item: an array, or an object whose values you visit in turn.
- Callback
- Think of it as a little instruction note you clip to the tool
- 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
- Think of it as the instruction a cook follows for each item on the tray
- 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
- Think of it as saying "the red ones" instead of explaining how to check each colour
- 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
- Think of it as cooking straight in the serving dish instead of a fresh bowl
- 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
- Think of it as an empty jar, which counts as "nothing here" when you check
- Values that count as no when a program asks yes or no: false, 0, empty text, null, undefined and NaN.
_.compactthrows all of them out of a list. - Removing duplicates
- Think of it as taking the second packet of cumin off the shelf
- Keeping only one of each item.
_.uniqdoes it for plain values._.uniqBydoes 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
- Think of it as a box inside a box inside a box
- 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
- Think of it as a spice rack with a name on every slot, so you grab the one you want
- An object where each record is filed under its id, so you can go straight to it instead of searching the whole list.
_.keyBybuilds 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
- Think of it as directions such as "top cupboard, left shelf, blue tin"
- A route to a value deep inside nested data, written with dots or as a list of steps.
_.getfollows one and gives a fallback instead of crashing when a step is missing. - For example
"data.user.city"is a path. - Merge
- Think of it as layering a second recipe's changes on top of the first
- 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
- Think of it as cooking a second, separate pot instead of pouring half into a bowl that shares the same stew
- 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
- Think of it as tasting two dishes to see if they match, rather than checking it's the same plate
- 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.
_.isEqualcompares the contents.
Kitchen timers
Tools that decide when a function runs and how often, so a busy page doesn't swamp the server.
- Debounce
- Think of it as a timer that restarts every time someone opens the oven, and only rings once they leave it alone
- 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
- Think of it as a turnstile that lets one person through each second however big the queue
- 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
- Think of it as jotting each answer in a notebook so you never work the same sum out twice
- 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)
- Think of it as a conveyor belt where each station does one job and passes the tray on
- Several steps joined so the output of one feeds the next.
_.chainputs your data on the belt and runs it when you ask for.value()._.flowbuilds the belt with no data on it, as a function you can reuse. - Curried
- Think of it as a machine you set up with the attachment first and feed ingredients into later
- 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
- Think of it as the knives that now come built into every new kitchen
- 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
- Think of it as packing a lunch box with only what you'll eat, not the whole fridge
- 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
- Think of it as someone slipping a bad ingredient into the shared stock pot that every dish draws from
- 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.
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.
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.
| Term | Means | Example |
|---|---|---|
| Collection | An array or an object you walk through | orders, { a: 1, b: 2 } |
| Iteratee | The function run on each item | (o) => o.total |
| Property shorthand | A key name instead of a function | "total" |
| Matches shorthand | An object pattern to match | { paid: true } |
| Pair shorthand | A key and a value to match | ["city", "Pune"] |
| Path | A dotted or array route into nested data | "data.user.city" |
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);
$ 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.
Arrays
Split, dedupe, compare and flatten lists without writing index maths.
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.
Splits into groups of size. The last group may be shorter.
See the exampleDrops falsy values: false, null, 0, "", undefined, NaN.
See the exampleKeeps the first item for each key.
See the exampleItems in the first array but not in the second.
See the examplePairs items by index, like a zipper.
See the exampleconsole.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]));
$ 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.
| Function | Does | Native close match |
|---|---|---|
_.chunk(xs, n) | Batches of n | None built in |
_.uniq(xs) | Unique primitives | [...new Set(xs)] |
_.uniqBy(xs, "id") | Unique by key | None built in |
_.flatten / _.flattenDeep | One level / all levels | xs.flat() / xs.flat(Infinity) |
_.head / _.last | First / last item | xs[0] / xs.at(-1) |
_.take(xs, n) | First n items | xs.slice(0, n) |
_.sortedUniq, _.pull, _.remove | Sorted dedupe, remove in place | pull and remove mutate |
Collections
Group, index, count, sort and split lists of records, the daily bread of report and API code.
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.
Key to array of items. Many per key.
See the exampleKey to one item. The last one wins.
See the exampleKey to how many items share it.
See the exampleMulti key sort with asc or desc per key.
See the exampleTwo arrays: items that pass and items that fail.
See the exampleTotals and extremes by a key.
See the exampleconst 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));
$ 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 have | You want | Write |
|---|---|---|
| Rows with a city | Rows per city | _.groupBy(rows, "city") |
| Users with ids | Find by id fast | _.keyBy(users, "id") |
| Events with a type | Count per type | _.countBy(events, "type") |
| Orders | Newest first, then biggest | _.orderBy(o, ["at", "total"], ["desc", "desc"]) |
| Jobs | Done and not done | _.partition(jobs, "done") |
Objects
Read deep paths safely, pick and drop keys, merge configs and compare or copy nested data.
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.
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)));
$ 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.
| Function | Mutates input | Use for |
|---|---|---|
_.get(obj, path, fallback) | No | Reading optional nested data |
_.set(obj, path, value) | Yes | Writing a deep path, creating levels |
_.pick / _.omit | No | Shaping responses |
_.merge(target, ...src) | Yes, target | Deep config layering |
_.defaults(target, ...src) | Yes, target | Fill only missing keys |
_.mapValues(obj, fn) | No | Transform every value |
_.cloneDeep(obj) | No | Independent copies |
_.isEqual(a, b) | No | Deep equality checks |
Function helpers
Control when and how often functions run: debounce, throttle, memoize and once.
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: 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());
$ 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.
| Helper | Useful options | Watch out for |
|---|---|---|
_.debounce(fn, ms, opts) | leading, trailing, maxWait | Call .cancel() on unmount or shutdown |
_.throttle(fn, ms, opts) | leading, trailing | Both edges fire by default |
_.memoize(fn, resolver) | Custom cache key via resolver | Cache grows forever; clear fn.cache |
_.once(fn) | None | Errors are not retried |
Strings and utilities
Convert naming styles, trim text for display, and reach for the small number and type helpers.
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.
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));
$ node strings.js orderCreatedAt order-created-at order_created_at User First Name Zod and Joi w... 007 <b>hi</b> [ 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.
| Input | Function | Output |
|---|---|---|
"order created at" | _.camelCase | orderCreatedAt |
"OrderCreatedAt" | _.kebabCase | order-created-at |
"orderCreatedAt" | _.snakeCase | order_created_at |
"user_first_name" | _.startCase | User First Name |
"hello" | _.upperFirst | Hello |
"<b>" | _.escape | <b> |
Chaining and flow
Compose several steps into one readable pipeline, either wrapped with chain or as a reusable function with flow.
_.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.
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));
$ 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.
Native vs lodash
Modern JavaScript absorbed many lodash classics. Know which ones, and import only what you still need.
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.
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);
$ 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.
| Lodash | Native today | Keep lodash when |
|---|---|---|
_.groupBy | Object.groupBy(xs, fn) | You need key shorthands |
_.flattenDeep | xs.flat(Infinity) | Never, native is fine |
_.uniq | [...new Set(xs)] | Never for primitives |
_.get | obj?.a?.b ?? fallback | The path is dynamic, like "a.b.c" |
_.cloneDeep | structuredClone(obj) | You clone functions or class instances |
_.last | xs.at(-1) | Never |
_.sortBy | xs.toSorted(cmp) | You sort by several keys |
_.isEqual | None | Always |
_.merge | Spread is shallow only | Always, for deep config |
_.debounce / _.throttle | None | Always |
Which one do I need?
Find your situation, take the tool in the middle column and copy the starting line.
| Situation | Reach for | Start with |
|---|---|---|
| Batch rows for a bulk API | Arrays | _.chunk(rows, 100) |
| Report totals per group | Collections | _.groupBy then _.sumBy |
| Look records up by id | Collections | _.keyBy(users, "id") |
| Read an optional nested field | Objects | _.get(res, "data.user.city", null) |
| Layer config files | Objects | _.merge({}, base, env, local) |
| Compare two payloads | Objects | _.isEqual(a, b) |
| Search as the user types | Functions | _.debounce(search, 300) |
| Rate limit a noisy handler | Functions | _.throttle(send, 1000) |
| snake_case columns to JSON | Strings | _.mapKeys(row, (v, k) => _.camelCase(k)) |
| Flatten, dedupe, clone simply | Native first | flat(), new Set(), structuredClone() |