Lodash _.tap() Method

Beginner
⏱️ 7 min read
📚 Updated: Jul 2026
🎯 6 Examples + 3 Try It
Seq & chaining

What You’ll Learn

By the end of this tutorial, you’ll use _.tap() to log, inspect, or run side effects inside Lodash chains without changing the pipeline value.

01

Side effects

Log mid-chain.

02

Pass-through

Value unchanged.

03

In chains

.tap(fn)

04

Standalone

_.tap(v, fn)

05

vs _.thru()

Observe vs transform.

06

Debug pipelines

Inspect steps.

What Is _.tap()?

_.tap() lets you peek into a Lodash chain, run a function for side effects, and continue with the same value. It is the go-to tool for debugging pipelines: log intermediate arrays after map, validate data before filter, or record metrics—all without altering what the next step receives.

💡
Pass-through, not transform

Your interceptor’s return value is ignored. _.tap() always returns the input value. To change the value in a chain, use _.thru() instead.

Use _.tap(value, interceptor) as a standalone function, or call .tap(interceptor) inside _.chain() / _(value) pipelines. Finish chains with .value() as usual.

📝 Syntax

Two forms—standalone and chained:

javascript
// Standalone
_.tap(value, interceptor)

// Inside a chain
_.chain(value)
  .map(fn)
  .tap(interceptor)
  .filter(fn)
  .value()

Syntax Rules

  • value — the data to pass through (standalone form only).
  • interceptor — function invoked with the current value; use for logging, validation, etc.
  • Return value — always the original input value, not what the interceptor returns.
  • Chain form.tap(fn) on a wrapper; returns the wrapper for more steps.
  • vs _.thru() — thru replaces the value with the interceptor’s return value.
javascript
import _ from "lodash";

const result = _.chain([1, 2, 3, 4])
  .map(function (num) { return num * 2; })
  .tap(function (arr) {
    console.log("after map:", arr);
  })
  .filter(function (num) { return num % 4 === 0; })
  .value();

console.log("final:", result);
// after map: [2, 4, 6, 8]
// final: [4, 8]

⚡ Quick Reference

TaskCode patternResult
Log mid-chain.tap(console.log)Same value continues
Custom debug.tap(function (v) { ... })Side effect only
Standalone tap_.tap(data, fn)Returns data unchanged
Transform value.thru(fn)See _.thru()
Start chain_.chain(data)See _.chain()
Unwrap.value()See .value()
Tap
.tap(fn)

Side effect, pass-through

Thru
.thru(fn)

Transform value

Debug
console.log

Common interceptor

🧰 Parameters

_.tap() accepts a value and an interceptor function:

value Required

The value to pass through. In chains, this is the current wrapped result from prior steps.

_.tap([1, 2, 3], fn)
interceptor Required

Function called with the value. Run logging, validation, or other side effects here.

function (v) { console.log(v); }
return Same value

Always returns the input value—the interceptor’s return is discarded.

// interceptor return ignored
chain .tap() Wrapper

In a chain, .tap(fn) returns the wrapper so map, filter, and other steps continue.

_.chain(x).tap(fn).map(...)

Need to change the value based on a custom function? Use _.thru(), not _.tap().

Examples Gallery

Practical _.tap() patterns with sample output and interactive Try It Yourself labs.

📚 Getting Started

Insert .tap() between chain steps to inspect intermediate results.

Example 1 — Debug a map/filter chain

Log the array after map before filter runs—the final result is unchanged by tap.

javascript
const result = _.chain([1, 2, 3, 4])
  .map(function (num) { return num * 2; })
  .tap(console.log)
  .filter(function (num) { return num % 4 === 0; })
  .value();

console.log(result);
// console (from tap): [2, 4, 6, 8]
// -> [4, 8]
Try It Yourself

How It Works

.tap(console.log) prints [2, 4, 6, 8] then passes that same array to filter. The pipeline output is still [4, 8].

Example 2 — Standalone _.tap(value, fn)

Run a side effect and still return the original value for assignment.

javascript
const user = { name: "Alex", role: "admin" };

const sameUser = _.tap(user, function (u) {
  console.log("saving:", u.name);
});

console.log(sameUser === user);
// -> true (same object reference)
Try It Yourself

📈 Practical Patterns

Validation hooks, comparison with thru, and reusable debug helpers.

Example 3 — Validation side effect

Assert a condition in tap without changing the data flowing to the next step.

javascript
function assertNonEmpty(arr) {
  if (!arr.length) {
    console.warn("empty array at this step");
  }
}

const result = _.chain([1, 2, 3])
  .map(function (n) { return n + 1; })
  .tap(assertNonEmpty)
  .take(2)
  .value();

console.log(result);
// -> [2, 3]

Example 4 — _.tap() vs _.thru()

Tap ignores the interceptor return; thru uses it as the new chain value.

javascript
const viaTap = _.chain([1, 2, 3])
  .tap(function () { return [99]; }) // return ignored
  .value();

const viaThru = _.chain([1, 2, 3])
  .thru(function (arr) { return [99]; }) // return used
  .value();

console.log(viaTap);
// -> [1, 2, 3]

console.log(viaThru);
// -> [99]
Try It Yourself

🚀 Beyond the Basics

Multiple inspection points and clean named debug functions.

Example 5 — Multiple tap points

Trace a pipeline at several stages without breaking the chain.

javascript
const log = function (label) {
  return function (v) {
    console.log(label + ":", v);
  };
};

const result = _.chain([5, 10, 15])
  .tap(log("start"))
  .map(function (n) { return n / 5; })
  .tap(log("after map"))
  .sum()
  .value();

console.log("sum:", result);
// start: [5, 10, 15]
// after map: [1, 2, 3]
// sum: 6

Example 6 — Named debug helper

Extract tap logic into a reusable function for cleaner pipelines.

javascript
function debugStep(name) {
  return function (value) {
    console.log("[debug " + name + "]", JSON.stringify(value));
  };
}

const evens = _.chain([1, 2, 3, 4, 5, 6])
  .filter(function (n) { return n % 2 === 0; })
  .tap(debugStep("evens"))
  .map(function (n) { return n * 10; })
  .value();

// [debug evens] [2,4,6]
// evens -> [20, 40, 60]

🧠 How _.tap() Works

1

Value arrives at tap

A prior chain step (or the initial wrap) produces the current value—often an array or object.

Input
2

Interceptor runs

Lodash calls your function with that value. Log, validate, or trigger side effects here.

Side effect
3

Same value passes through

The interceptor’s return is discarded. The original value continues to the next chain step.

Pass-through
=

Pipeline unchanged

Downstream filter, map, and .value() see the same data as if tap were not there.

📝 Notes

  • _.tap() is for observation and side effects—not for transforming data (use _.thru() or map).
  • The interceptor’s return value is always ignored; only side effects matter.
  • In chains, .tap(fn) returns a wrapper so subsequent steps work normally.
  • Pair with _.chain() and end with .value() like any other pipeline.
  • Remove or gate verbose debug taps before shipping—or replace with structured logging.
  • Next in the series: _.thru() when you need a custom transform inside the chain.

Conclusion

_.tap() is the Lodash tool for side effects inside chains—log intermediate results, run validation, or trigger hooks—while keeping the pipeline value unchanged. Remember: tap observes, thru transforms.

Next in the Seq series: _.thru(), which applies a custom function and replaces the chain value with the function’s return.

💡 Best Practices

✅ Do

  • Use .tap() to debug map/filter pipelines during development
  • Name interceptors clearly (debugStep("after map")) for readable traces
  • Keep tap functions focused on logging, metrics, or validation—not heavy I/O in hot paths
  • Use _.thru() when the interceptor should change the value
  • Finish chains with .value() after your last transform step

❌ Don’t

  • Return a new value from tap expecting it to flow downstream—use thru instead
  • Mutate the tapped value unless you intend side effects on shared references
  • Leave noisy console.log taps in production without a reason
  • Use tap when a simple map or filter is the real transform you need
  • Confuse tap with _.forEach()—tap is for chain pass-through semantics

Key Takeaways

Knowledge Unlocked

Five things to remember about _.tap()

Use these when adding side effects to Lodash chains safely.

5
Core concepts
02

Pass-through

Value unchanged.

Critical
🔄 03

In chains

.tap(fn)

Pattern
🔀 04

vs thru

Observe vs transform.

Compare
🛠️ 05

Debug pipelines

Mid-chain peek.

Guideline

❓ Frequently Asked Questions

It calls your interceptor function with the current value, then returns that same value unchanged. Use it for logging, debugging, or side effects inside a chain.
No. The return value of your interceptor is ignored. _.tap() always passes the input value through to the next chain step.
_.tap() runs a side-effect function and keeps the original value. _.thru() runs a function and replaces the value with whatever the function returns.
Yes. _.chain(data).map(...).tap(fn).filter(...).value() is the most common pattern—tap inspects the value mid-pipeline.
Call _.tap(value, interceptor) outside a chain when you want to run a side effect on a value and still get that same value back for assignment or return.
Debug taps are fine temporarily; for permanent logging or metrics, keep taps focused and intentional—or extract side effects to named functions for clarity.
Did you know?

The name tap comes from pipeline engineering—like a inspection valve on a pipe. Data flows through unchanged while you “tap into” it to observe what is passing by.

Practice _.tap() in the Live Editor

Debug map/filter chains and compare tap with thru behavior.

Open Try It editor →

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