Lodash _.merge() method

Beginner
⏱️ 8 min read
📚 Updated: May 2026
🎯 3 Code examples
🚀 3 Try-it labs
Lodash

What you’ll learn

  • How _.merge(object, ...sources) recursively merges plain objects into the destination.
  • Why it mutates the first argument—and the _.merge({}, a, b) pattern that sidesteps it.
  • How undefined, null, and array values behave when merging.
  • When to switch to _.mergeWith() for array concatenation or custom rules.

Prerequisites

You should know the difference between a shallow copy (_.assign, spread) and a deep recursive merge, and understand object identity vs. value.

  • Mutates the destination: the first argument is written into and also returned.
  • Recursive for plain objects: nested objects are merged property by property, not replaced wholesale.
  • Arrays merge by index: rarely what you want. _.mergeWith exists for a reason.

Overview

_.merge walks each source object’s own enumerable string-keyed properties (including inherited ones) and writes them into the destination. When both sides hold plain objects at the same key, it recurses. The most important fact to internalize: the destination is mutated, so the _.merge({}, defaults, overrides) idiom is the standard way to get a fresh result.

Deep, recursive

Nested plain objects are merged property by property instead of replaced.

Mutates & returns

The first argument absorbs the merge result and is the function’s return value.

undefined is skipped

Source undefined never overwrites; null, 0, "" all do.

Syntax

javascript
_.merge(object, [...sources])
  • object: the destination, which is mutated and returned.
  • sources: one or more source objects merged in left-to-right order. Later sources override earlier ones at the same key.
  • Returns: the same destination object reference, now containing the merged result.
1

Deep merge with default config

The defaults-and-overrides pattern—use _.merge({}, defaults, overrides) so the defaults object survives intact.

javascript
import merge from "lodash/merge";

const defaults = {
  theme: { mode: "light", fontSize: 16 },
  features: { analytics: true, beta: false }
};

const overrides = {
  theme: { mode: "dark" },
  features: { beta: true },
  user: "ada"
};

const config = merge({}, defaults, overrides);
// -> {
//      theme:    { mode: "dark", fontSize: 16 },   // deep-merged
//      features: { analytics: true, beta: true },  // deep-merged
//      user:     "ada"                             // added
//    }

defaults.theme.mode;
// -> "light"   (defaults object is untouched thanks to the {})
Try it Yourself
2

Mutation: the destination is the result

If you forget the {} first argument, your "destination" is the thing being modified. This is a frequent source of unexpected state changes in reducers and selectors.

javascript
import merge from "lodash/merge";

const user = {
  name: "John",
  address: { city: "New York", country: "USA" }
};

const updates = {
  age: 35,
  address: { city: "San Francisco" }
};

const result = merge(user, updates);

result === user;
// -> true   (same reference)

user.address.city;
// -> "San Francisco"   (user was mutated)

// Immutable-style pattern:
const fresh = merge({}, user, updates);
fresh === user;
// -> false  (new object)
Try it Yourself
3

Arrays merge by index; undefined is skipped

Two non-obvious rules that bite developers when merging real data. Arrays are merged positionally (not concatenated), and an undefined source value won’t overwrite the destination.

javascript
import merge from "lodash/merge";
import mergeWith from "lodash/mergeWith";

// Arrays merge positionally (NOT concatenated):
merge({}, { items: [1, 2, 3] }, { items: [10] });
// -> { items: [10, 2, 3] }

// To concatenate, use _.mergeWith:
mergeWith({}, { items: [1, 2, 3] }, { items: [10] }, (objValue, srcValue) =>
  Array.isArray(objValue) ? objValue.concat(srcValue) : undefined
);
// -> { items: [1, 2, 3, 10] }

// undefined is skipped, null overwrites:
merge({}, { name: "John", role: "admin" }, { name: undefined, role: null });
// -> { name: "John", role: null }
//    name kept because undefined was skipped;
//    role overwritten because null is an explicit value.
Try it Yourself

📋 _.merge vs related operations

Topic_.merge_.mergeWith_.assignSpread { ...a, ...b }
DepthRecursiveRecursiveShallowShallow
Mutates first argYesYesYesNo
ArraysMerged by indexCustomizer decidesReplacedReplaced
undefined sourcesSkippedSkipped (unless customizer returns)OverwritesOverwrites
Walks prototypeYes (enumerable)Yes (enumerable)No (own only)No (own only)

Use _.merge for deep configuration objects with primitives and plain objects. Use _.mergeWith when arrays need to concatenate or any field needs custom semantics. Use _.assign or spread when you want a shallow, last-write-wins overlay.

Pitfalls to avoid

Mutation

Don’t merge into shared state

If defaults is a module-level constant, _.merge(defaults, overrides) permanently rewrites it. Always use _.merge({}, defaults, overrides) in reducers and selectors.

Arrays

Positional array merge is rarely what you want

The old reference incorrectly claimed _.merge doesn’t handle arrays. It does—by index. For concat or replace semantics, switch to _.mergeWith or pre-process arrays.

Prototype

Prototype pollution surface

Never feed untrusted JSON straight into _.merge targeting a shared object. Keys like __proto__ or constructor.prototype in attacker-controlled sources can poison the runtime.

Cycles

Circular references explode

Cyclic graphs lead to a stack overflow. Detect cycles up front or flatten the structure before merging.

❓ FAQ

Yes. The destination object is mutated in place and also returned. Pass an empty object as the first argument when you want an immutable-style result: _.merge({}, a, b).
_.assign performs a shallow copy of own enumerable properties: nested objects are overwritten whole. _.merge recurses into plain objects, merging them property by property.
Yes, but positionally by index. _.merge({ items: [1,2,3] }, { items: [10] }) gives { items: [10, 2, 3] }, not concatenation. For concatenation, use _.mergeWith with a customizer that returns objValue.concat(srcValue).
An undefined source value is ignored and the destination value is kept. A null source value overwrites the destination, the same as any other primitive.
Yes. _.merge(dest, src1, src2, ...) walks sources left to right; later sources override earlier ones.
No. Cycles in the input objects cause a stack overflow. Pre-flatten or sanitize cyclic graphs before merging.

Summary

Did you know?

_.merge mutates the first argument and returns it. Pass _.merge({}, defaults, overrides) when you need an immutable-style result—the empty {} absorbs the writes.

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

6 people found this page helpful