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:
thenis the right method for method chains;tapis the right method for debugging and side effects. The line blurs when usingtapfor mutation, but a good heuristic: if the block’s return value matters, usethen. If you’re only interested in what happens inside the block, usetap. 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.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀