skip to content

In Ruby, what is the difference between `Kernel#tap` and `Kernel#then` in a method chain, and when would you use each?

level: middleimportance: must knowfreq 55%

answer

  1. one returns the receiver
  2. the other returns the block's value
  3. yield_self is the old name
  4. block result ignored by tap
  5. no block: Enumerator vs LocalJumpError

basics

~20 s

Both yield the receiver to a block. tap returns the receiver itself, ignoring the block's value, so it suits side effects such as logging mid-chain; then (alias yield_self) returns the block's value, so it transforms or pipes the value onward.

solid answer

~40 s

`obj.tap { |x| ... }` yields `obj` and then returns **`obj`**, whatever the block returned. Use it to observe or configure without changing what flows down the chain: `title.strip.tap { |t| logger.debug(t) }.squeeze(" ")`. `obj.then { |x| ... }` yields `obj` and returns **the block's result**, so the chain continues with a new value: `title.then { |t| t.empty? ? "Untitled" : t }`. `yield_self` is an alias of `then`. The classic trap is `title.tap { |t| t.upcase }`, which returns the original title because the upcased copy is discarded; `tap { |t| t.upcase! }` does change it, because it mutates the same object. Without a block, `then` returns an Enumerator, while `tap` raises `LocalJumpError` since it always yields.

code

ruby · 11 lines
ruby
title = "red mug".dup

title.tap { |t| t.upcase }    # => "red mug"  block value discarded
title.then { |t| t.upcase }   # => "RED MUG"  block value returned
title                         # => "red mug"

title.tap { |t| t.upcase! }   # => "RED MUG"  same object, mutated
title                         # => "RED MUG"

"".then { |t| t.empty? ? "Untitled" : t }  # => "Untitled"
"x".then.class                              # => Enumerator

go deeper

for a junior

Recall that tap returns the receiver and then returns the block's value, and that yield_self is another name for then.

for a middle

Explain why tap with a non-mutating call does nothing visible, why tap with a bang call does, and how then pipes a value into helpers and branches.

for a senior

Use tap and then to keep long transformation chains readable and debuggable, and recognise when a named local or a plain method call is clearer than either.

for a principal

Decide how far a team leans on then-style pipelines versus named intermediate steps, weighing reading flow against ease of debugging.

## Two methods, one difference Both methods are defined in `Kernel`, so every ordinary object has them (only `BasicObject` instances lack them), and both **yield the receiver to a block**. They differ only in what they **return**: | | `tap` | `then` (alias `yield_self`) | |---|---|---| | Yields | the receiver | the receiver | | Returns | the receiver, always | the block's return value | | Block's value | ignored | becomes the new value in the chain | | Without a block | `LocalJumpError` (it yields unconditionally) | an `Enumerator` | | Typical use | side effects: log, assert, configure | transformation: pipe into a function, branch | In Ruby's own source, `tap` is essentially `yield(self)` followed by `self`, while `then` returns the value of `yield(self)`. ## `tap`: look without changing the flow `tap` "taps into" a chain to act on an intermediate value while passing the same object on: ```ruby clean = raw_title .strip .tap { |t| warn "after strip: #{t.inspect}" } .squeeze(" ") ``` - The chain's value after `tap` is exactly what it was before, so removing the `tap` line changes nothing else. - It is also the idiom for configuring and returning a new object in one expression: `Product.new.tap { |p| p.title = clean }` returns the product, not the String assigned. - Because the same object is yielded, a **mutating** call inside `tap` is visible downstream: `tap { |t| t.upcase! }` changes the String, while `tap { |t| t.upcase }` builds a copy and throws it away. ## `then`: pipe the value somewhere new `then` lets a chain continue through code that is **not** a method of the current object, or through a branch: ```ruby display = raw_title .strip .squeeze(" ") .then { |t| t.empty? ? "Untitled product" : t } .then { |t| truncate_title(t, 60) } ``` - `truncate_title` is a helper that takes the String as an argument; `then` fits it into the left-to-right flow instead of wrapping the whole chain in `truncate_title(...)`. - Each `then` replaces the value, so the next link works on whatever the block returned, including a different type. - `yield_self` is the same method under its older name. RuboCop's pending `Style/ObjectThen` cop enforces one spelling, `then` by default. ## Choosing between them 1. Ask what the next link should receive. The **same object**: `tap`. A **computed value**: `then`. 2. If the block's value matters and you used `tap`, the value is silently lost; if you used `then` for a side effect such as logging, the chain continues with the logger's return value, often `nil` or `true`. 3. Prefer a plain method call when one exists: `title.strip.then { |t| t.squeeze(" ") }` is just `title.strip.squeeze(" ")`. ## The blockless forms `then` without a block returns an `Enumerator` over the single receiver, which enables short-circuit tricks from the core docs such as `2.then.detect(&:odd?)`, returning `nil` when the condition fails. `tap` has no blockless form; calling it without a block raises `LocalJumpError` because its body always yields. Neither form is common in application code, but the difference shows up in interview follow-ups. ## Common misconceptions - "`tap` returns whatever the block returns." That is `then`. - "`tap` works on a copy." It yields the receiver itself, so mutations stick. - "`then` mutates the receiver." It only passes it to the block; any change depends on what the block does. ## How to answer in an interview Lead with the one-line contrast: both yield the receiver, `tap` returns the receiver, `then` returns the block's value. Give one example of each from the same pipeline, a logging `tap` and a fallback `then`, and then name the trap that shows you have used them: `tap { |t| t.upcase }` does nothing visible, while `tap { |t| t.upcase! }` changes the object because the block receives the receiver itself, not a copy.

  • Why does `Product.new.tap { |p| p.title = "Mug" }` return the product and not the String?
    `tap` always returns its receiver, here the new `Product`, and ignores the block's value. The assignment inside the block evaluates to `"Mug"`, but that value is discarded. With `then` the same expression would return `"Mug"`, which is why `tap` is the idiom for configure-and-return.
  • What goes wrong if you log with `then` in the middle of a chain?
    `then` returns the block's value, so the chain continues with whatever the logging call returned, for example `nil` from `warn`, and the next link fails or works on the wrong value. Logging mid-chain is exactly what `tap` is for, because it passes the original object on.

tap is a quality inspector on a conveyor belt: they look at the box, maybe add a sticker, and put the same box back on the belt. then is a repacking station: whatever it packs becomes the box that moves on, even if it is a completely different box.

saying these in an interview costs you the question

  • tap returns the value computed inside its block.
  • tap yields a copy, so mutating it inside the block has no effect.
  • then and yield_self are two different methods with different behaviour.
  • then is the right tool for logging in the middle of a chain.
  • Calling tap without a block returns an Enumerator.