Kernel#then
obj.then { |x| block } -> result then passes the receiver to a block and returns the block’s result. It enables fluent method chaining on objects that don’t otherwise support it, letting you transform values in a pipeline style.
Syntax
object.then { |x| block } -> result
Parameters
| Parameter | Type | Description |
|---|---|---|
block | Proc | A block that receives the object and returns a transformed value |
The block receives the receiver as its argument, so you can name the parameter whatever fits the context. Since then is defined on Object, every value in Ruby responds to it, making it a universal tool for inserting a transformation step anywhere in a chain.
Examples
Basic transformation
# Square a number
5.then { |x| x * x }
# => 25
# Chain multiple transformations
10.then { |x| x + 1 }.then { |x| x * 2 }
# => 22
Each call to then feeds the result of the previous block into the next, so the value flows through the chain without temporary variables. This is the same idea behind the pipeline operator in Ruby 3.0+, where then is the block-receiving end of the pipe.
Method chaining with pipeline operator
# Ruby 3.0+ pipeline operator
1 |> then { |x| x + 10 } |> then { |x| x * 2 }
# => 22
# Equivalent using then
1.then { |x| x + 10 }.then { |x| x * 2 }
# => 22
The pipeline operator |> passes its left-hand value as the first argument to the right-hand method. When paired with then, it creates a left-to-right syntax that reads in execution order without nesting calls inside parentheses. The operator is purely syntactic sugar, so you can use plain then chains in older Ruby versions and get the same result.
Transforming data structures
# Convert hash to array of values
{ a: 1, b: 2 }.then { |h| h.values }
# => [1, 2]
# Transform and return the original type
[1, 2, 3].then { |arr| arr.map { |x| x * 2 } }
# => [2, 4, 6]
When the block returns a different type than the input, then keeps the chain going with the transformed value. Working with hashes and arrays this way keeps intermediate variables from cluttering the method scope, which is especially useful in pipelines that convert between data shapes.
Common Patterns
Tap alternative
then is similar to tap but returns the block’s result instead of the receiver:
# tap returns self
result = "hello".tap { |s| s.upcase! }
# => "HELLO" (result is the modified string)
# then returns the block's value
result = "hello".then { |s| s.upcase }
# => "HELLO" (result is the upcased string)
The difference between tap and then is straightforward: tap always returns the receiver after the block runs, while then returns whatever the block itself produces. Use tap for side effects like logging or initialization, and then when the block creates a new value you need for the next step.
Conditional transformation
user_input.then { |input| input.empty? ? nil : input.strip }
A one-line conditional inside then keeps the logic close to the value it transforms. When the input is empty the method returns nil, and otherwise it strips whitespace — a compact pattern for sanitizing user data without a separate guard clause.
Object initialization pattern
# Build an object with transformations
User.new.then do |user|
user.name = "Alice"
user.email = "alice@example.com"
user
end
The object initialization pattern is occasionally useful when you want to build and configure an object in a single expression. The block sets attributes and returns the object, making it the result of the then call. This is less common than assigning to a variable, but it can be clean in DSL-style contexts where brevity matters.
Validation and transformation
# Validate then transform
data.then do |input|
raise "Invalid" unless input.valid?
input.to_json
end
Chaining validation and transformation in one then block keeps the happy path linear: validate, transform, return. If validation fails the method raises, which stops the chain. This is a cleaner alternative to nested if statements when the error case is truly exceptional and should halt execution.
Errors
then raises LocalJumpError if no block is given:
5.then
# => LocalJumpError: no block given
keeping pipelines readable
then works best when each step has a clear job and the block stays small enough to scan without effort. It can make the code read like a short data pipeline, especially when one object needs to be transformed before the next method runs. If the block begins to contain several branches or a lot of temporary state, a named helper is often easier to follow. The method is strongest when it trims ceremony, not when it hides complex work.
That is why then often pairs well with parsing, mapping, and small cleanup steps. Each call can represent one decision or one transformation, which keeps the chain easy to inspect. The method is not trying to replace every helper or every method call. It simply gives the code a neat place to put the next step when the value is already in hand and the transformation is the main thing the reader needs to understand.
It can also make examples feel more linear because the data moves through the chain in the same order the reader sees it. That helps when a single value needs a few modest changes before it is ready to use. If the chain starts to feel overloaded, the code is usually telling you that one of the steps deserves its own method name instead of being hidden in the middle of the pipeline.