rubyguides

Object#tap

obj.tap { |x| block } -> obj

tap yields the receiver to a block and then returns the object itself. This simple method is incredibly useful for debugging and method chaining without creating intermediate variables.

Syntax

obj.tap { |x| block }

The block receives the object as its only argument, and whatever the block returns is simply discarded — tap always passes the original object through. This guarantee of returning the receiver is what makes tap useful in method chains where you need a side effect without changing the value.

Parameters

ParameterTypeDefaultDescription
blockBlockrequiredA block that receives the object as its parameter

Examples

Basic usage

# tap returns the object itself
message = "Hello, World!"
message.tap { |m| puts m }
# Hello, World!
# => "Hello, World!"

The output shows both the puts side effect and the return value on the same line, which is how IRB or Pry displays the result. The printed message appears first because puts writes to stdout before the REPL prints the return value, but the object itself is unchanged by the tap block.

Debugging method chains

# tap shines when debugging pipelines
result = [1, 2, 3, 4, 5]
  .map { |x| x * 2 }
  .tap { |arr| puts "After map: #{arr.inspect}" }
  .select { |x| x > 4 }
  .tap { |arr| puts "After select: #{arr.inspect}" }

# After map: [2, 4, 6, 8, 10]
# After select: [6, 8, 10]
# => [6, 8, 10]

Each tap call in the chain prints the intermediate state without interfering with the pipeline. The block inspects the value at that point and then the next method in the chain receives the same array, keeping the debugging code separate from the transformation logic.

Setting up objects

# Configure an object in a single expression
config = {}.tap do |h|
  h[:host] = "localhost"
  h[:port] = 3000
  h[:env] = "development"
end
# => { host: "localhost", port: 3000, env: "development" }

Using tap to build a hash or configure an object in a single expression keeps the setup compact. The object is created, populated inside the block, and returned — all without a temporary variable that would only exist for the duration of the configuration.

Common Patterns

Debugging in pipelines

tap shines when you need to inspect values mid-chain without disrupting the flow:

# Inspect without breaking the chain
data = [1, 2, 3]
  .map { |x| x ** 2 }
  .tap { |arr| puts "Squared: #{arr}" }
  .sum
# Squared: [1, 4, 9]
# => 14

Chaining tap after a sum call prints the final result while still returning it, so the pipeline works the same way with or without the debug step. Removing the tap line leaves the computation identical, which makes it safe to add and remove during development.

Initializing objects

class Connection
  attr_accessor :host, :port, :ssl
end

conn = Connection.new.tap do |c|
  c.host = "localhost"
  c.port = 3000
  c.ssl = true
end

When tap is a good fit

tap works best when you want to inspect or configure an object without breaking a chain. It is useful in setup code, debugging, and small factories where the object should keep flowing through the expression unchanged. The key is to keep the block focused on side effects that are easy to understand at a glance. If the block starts doing real work or branching heavily, a normal method or a temporary variable is usually clearer and easier to maintain.

Another good use is the small setup sequence that would otherwise need several assignment lines before the value is returned. In that situation, tap keeps the object visible without hiding the last expression. It can make tests and builders easier to read because the final value still comes from the same chain the reader started with. When a chain becomes long, though, the goal should still be clarity, not cleverness.

If you use tap to inspect an object, keep the print or log step short so the code still reads as one flow. A little visibility is useful, but too much work inside the block turns the chain into a puzzle. The method is at its best when it gives you just enough structure to stay oriented and then gets out of the way.

If the chain needs more than a quick glance or a few assignments, the code is usually asking for a named helper instead. That keeps the flow readable and gives the next reader a clear place to put a breakpoint or a log statement. tap is best when the block remains a small pause, not a second program inside the first one.

See Also