How Lodash defaultsDeep Handles Nested Missing Properties

In the Lodash JavaScript library, _.defaultsDeep provides a robust way to fill in default values for complex, nested data structures. This article explains how _.defaultsDeep recursively traverses objects to populate missing nested properties while strictly preserving existing values, how it differs from shallow merging utilities, and how it treats edge cases like falsy values.

The Core Mechanism of _.defaultsDeep

Unlike shallow assignment methods like Object.assign or Lodash's standard _.defaults, which only evaluate top-level keys, _.defaultsDeep traverses objects recursively. It accepts a destination object followed by one or more source objects containing default values, proceeding from left to right.

During traversal, the function inspects each nested property:

  1. Existence Check: It checks whether the property key exists on the destination object at the current depth.
  2. undefined Evaluation: If the property exists and its value is not undefined, _.defaultsDeep leaves the property untouched.
  3. Recursive Descent: If both the destination property and the corresponding source property contain plain objects, the method steps into that sub-tree rather than performing a direct assignment.
  4. Insertion: If a key does not exist on the destination object or resolves strictly to undefined, the corresponding value from the source object is copied over.

Why Shallow Defaults Fail with Nested Objects

A standard shallow default method replaces entire object references rather than merging them. Consider an existing user configuration object:

const userConfig = {
  theme: {
    mode: 'dark'
  }
};

const defaultConfig = {
  theme: {
    mode: 'light',
    fontSize: 14
  }
};

Using a shallow method like _.defaults(userConfig, defaultConfig) checks if userConfig.theme exists. Because theme already exists as an object, the entire default theme object is ignored, leaving fontSize completely missing.

_.defaultsDeep(userConfig, defaultConfig) solves this by navigating inside theme. It recognizes that userConfig.theme.mode is already defined as 'dark' and leaves it alone, but sees that userConfig.theme.fontSize is undefined. It copies only fontSize: 14 into userConfig.theme.

The resulting object becomes:

{
  theme: {
    mode: 'dark',
    fontSize: 14
  }
}

Handling of Falsy Values and null

A critical aspect of _.defaultsDeep is its strict reliance on the undefined check to prevent accidental overwrites:

Mutability and Cloning

By default, _.defaultsDeep mutates the first object passed to it. To avoid mutating the original data structure, pass an empty object as the first argument:

const finalConfig = _.defaultsDeep({}, userConfig, defaultConfig);

This pattern returns a new object containing the merged results while leaving userConfig and defaultConfig intact.