/ Tags: RUBY 3 / Categories: RUBY

tap, then, and yield_self — Ruby's Method Chaining Toolkit

Ruby has three methods that look similar on the surface but solve distinct problems in method chains: tap, then (aliased as yield_self), and itself. Once you understand what each one is actually for, you’ll start reaching for them in situations where you’d previously have broken the chain into multiple statements — and you’ll appreciate why the chain is often cleaner.

tap — Inspect Without Breaking the Chain


tap yields the receiver to a block and then returns the receiver unchanged. The block’s return value is ignored. It’s designed for side effects — logging, debugging, mutation — while keeping a chain intact.

Example:

result = User.new(name: "Rajan", email: "[email protected]")
  .tap { |u| puts "Before save: #{u.inspect}" }
  .tap { |u| u.role = "admin" }
  .save!

Without tap, you’d assign to a variable, mutate it, log it, and then save — four lines that obscure the intent. With tap, the mutation and logging are inline and the return value stays on track.

This is where tap earns its place in production code: debugging a chain without restructuring it:

Example:

# Find the problem in a transformation chain without breaking it
orders
  .select(&:fulfilled?)
  .tap { |o| Rails.logger.debug("After fulfilled filter: #{o.count}") }
  .map(&:total_price)
  .tap { |prices| Rails.logger.debug("Prices: #{prices.inspect}") }
  .sum

Add the tap lines when investigating, remove them when done. The chain structure doesn’t change.

then / yield_self — Transform the Chain


then (introduced in Ruby 2.6, aliased as yield_self) is the functional counterpart: it yields the receiver to a block and returns whatever the block returns. Unlike tap, the return value matters — it replaces the receiver in the chain.

Example:

"[email protected]"
  .then { |email| User.find_by(email: email) }
  .then { |user| user&.generate_token }
  .then { |token| token ? { success: true, token: token } : { success: false } }

Each then transforms the value into something new. The chain reads like a data pipeline: take an email, find the user, generate a token, build a response. No intermediate variables, no conditional breaks in the flow.

This becomes especially useful when you want to pipe a value through a series of operations that don’t share a common class:

Example:

params[:user_id]
  .then { |id| Integer(id) rescue nil }
  .then { |id| id && User.find_by(id: id) }
  .then { |user| user&.admin? ? :admin : :guest }

Parsing an integer, finding a record, determining a role — three different concerns, three different return types, one readable chain.

then vs assignment

Before:

raw = params[:amount]
parsed = raw.to_f
clamped = [[parsed, 0.0].max, 1000.0].min
formatted = "$%.2f" % clamped

After:

formatted = params[:amount]
  .to_f
  .then { |n| [[n, 0.0].max, 1000.0].min }
  .then { |n| "$%.2f" % n }

Same logic, but the chain makes the transformation sequence explicit. The intermediate variables parsed and clamped were just scaffolding — then removes the need for them.

Conditional Chains with then


then enables conditional transformations without breaking the chain:

Example:

def process(value, apply_discount:)
  value
    .then { |v| apply_discount ? v * 0.9 : v }
    .then { |v| v.round(2) }
    .then { |v| "$#{v}" }
end

process(100.0, apply_discount: true)   # => "$90.0"
process(100.0, apply_discount: false)  # => "$100.0"

The conditional lives inline as a ternary inside then. The outer method stays linear.

itself — The Identity Pass-Through


itself returns the receiver without yielding to a block. It sounds useless until you see the problem it solves: converting method chaining to block-style when you need a Proc of the identity function.

Example:

# Count truthy values in an array
[1, nil, "hello", false, :symbol].count(&:itself)
# => 3

# Select non-nil elements (equivalent to compact)
[1, nil, 2, nil, 3].select(&:itself)
# => [1, 2, 3]

# Group by truthiness
[1, nil, false, "yes", 0].group_by(&:itself)
# => {1=>[1], nil=>[nil], false=>[false], "yes"=>["yes"], 0=>[0]}

&:itself converts itself to a block that passes each element through unchanged, letting you use it anywhere Ruby expects a block.

Pro-Tip: then is the right method for method chains; tap is the right method for debugging and side effects. The line blurs when using tap for mutation, but a good heuristic: if the block’s return value matters, use then. If you’re only interested in what happens inside the block, use tap. When in doubt about which makes the code clearer, write it both ways and read them aloud — the more natural-sounding one is usually right.

Combining All Three


Example:

def enrich_user_data(user_id)
  user_id
    .then  { |id| User.find_by(id: id) }
    .tap   { |u| Rails.logger.info("Found user: #{u&.email}") }
    .then  { |u| u&.attributes&.slice("name", "email", "role") }
    .then  { |attrs| attrs&.merge(fetched_at: Time.current) }
end

then drives the transformations. tap inserts a log without affecting the chain. Each step is clear about what it does and what it returns.

Conclusion


tap, then, and itself are small methods with focused responsibilities. They don’t replace variables or conditional logic everywhere — they complement them. Where method chains read naturally, these tools keep the chain intact while enabling debugging, transformation, and conditional logic. Learn them once, and you’ll find situations for them constantly. Ruby’s expressiveness comes from exactly this kind of composable, purposeful API design.

FAQs


Q1: Is then the same as yield_self?
Yes, exactly. then was introduced as an alias in Ruby 2.6 because yield_self is accurate but verbose. Both are available in all Ruby versions since 2.6. Use then in new code — it’s shorter and reads more naturally in chains.

Q2: Can I use tap to return a different value from a chain?
No. tap always returns the original receiver, ignoring the block’s return value. If you need to transform the value, use then. If you accidentally use tap where you needed then, the chain will silently pass the original value forward — watch for this bug.

Q3: Is chaining with then slower than explicit variables?
There’s a negligible method-call overhead for each then. In any real application context, this is irrelevant. Only optimize this if profiling shows these method calls as a bottleneck — which is extremely unlikely.

Q4: Does tap work on frozen objects?
tap works — it returns the frozen receiver after yielding it to the block. If your block tries to mutate the frozen object, Ruby raises FrozenError. Use tap on frozen objects only for read-only side effects like logging.

Q5: When should I prefer regular variables over then chains?
When the logic is complex enough that names add clarity. user_id.then { ... }.then { ... }.then { ... } with three complex blocks is harder to read than three well-named variables. then shines when each transformation is simple and the chain itself is the documentation. When intermediate names help, use them.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More