Lodash Chaining: Map and Filter Evaluation Sequence

This article examines the exact evaluation sequence when chaining multiple map and filter methods in the Lodash JavaScript library. It explains the mechanics of Lodash's lazy evaluation and shortcut fusion, contrasts this behavior with native JavaScript execution, and details the step-by-step path each array element takes through an operation pipeline before resolving the final result with .value().


Eager vs. Lazy Evaluation: The Core Mechanism

In native JavaScript (Array.prototype.map and Array.prototype.filter), chained operations execute eagerly using horizontal processing:

  1. The first method processes every single element in the array and allocates a new intermediate array.
  2. The second method runs on the newly created array, again processing every element and allocating another intermediate array.

Lodash explicit chains (created via _(collection) or lodash(collection)) operate differently using lazy evaluation and shortcut fusion. Lodash does not process operations horizontally. Instead, it fuses compatible operations—specifically map and filter—into a unified pipeline and processes data vertically, element by element.

The Exact Lifecycle of Chained Execution

When you chain map and filter calls in Lodash, execution proceeds through three distinct phases:

1. Deferred Pipeline Registration

Calling _(array).map(fn1).filter(fn2).map(fn3) does not execute any iteratees immediately. Instead:

2. Triggering Execution via .value()

Execution begins only when .value() (or its alias .run()) is invoked on the chain wrapper. This triggers the internal baseWrapperValue function.

3. Shortcut Fusion (Element-by-Element Processing)

Once triggered, Lodash reads from the original input array sequentially. Rather than applying map to every item, Lodash runs one single element through the entire chain of iteratees before moving to the next element:

  1. Read Element i: Lodash retrieves element i from the source collection.
  2. First Operation (e.g., map): The element is transformed by the iteratee. The transformed value is temporarily held in memory.
  3. Second Operation (e.g., filter): The transformed value is evaluated against the filter's predicate:
    • If the predicate returns truthy: The value proceeds immediately to the next operation in the chain for this same element.
    • If the predicate returns falsy: Lodash halts the pipeline for this element immediately. The remaining downstream map or filter functions are never called for this element. The engine discards it and proceeds directly to element i + 1.
  4. Subsequent Operations: Any further map or filter operations run sequentially on that single element.
  5. Collection: If the element passes all filter predicates, the final output value is appended to the final result array.
  6. Next Iteration: The process repeats for element i + 1.

Concrete Execution Trace

Consider the following chain:

const result = _([1, 2, 3])
  .map(n => {
    console.log(`map1: ${n}`);
    return n * 2;
  })
  .filter(n => {
    console.log(`filter: ${n}`);
    return n > 2;
  })
  .map(n => {
    console.log(`map2: ${n}`);
    return n + 10;
  })
  .value();

The exact sequence of execution printed to the console is:

  1. map1: 1 — Element 1 is multiplied to 2.
  2. filter: 22 > 2 evaluates to false. Lodash drops the element immediately. map2 is never called for 1.
  3. map1: 2 — Element 2 is multiplied to 4.
  4. filter: 44 > 2 evaluates to true. Pipeline continues for this element.
  5. map2: 4 — Element 4 is transformed to 14 and placed in the output array.
  6. map1: 3 — Element 3 is multiplied to 6.
  7. filter: 66 > 2 evaluates to true. Pipeline continues for this element.
  8. map2: 6 — Element 6 is transformed to 16 and placed in the output array.

Exceptions to Shortcut Fusion

This fused, element-by-element sequence applies strictly to operations that can be evaluated independently per element. If an operation requiring global knowledge of the collection (such as sortBy, reverse, or shuffle) is inserted between map and filter, shortcut fusion breaks at that boundary: