Ruby Blocks and Procs — yield Proc.new Call and Block Passing Explained
In this tutorial, you will learn about Ruby Blocks and Procs. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby blocks are anonymous code blocks passed to methods using yield or &block, Proc objects encapsulate blocks as reusable objects, and the & operator converts between blocks and procs.
What You'll Learn
- Defining and using blocks with yield
- Converting blocks to Proc objects with &block
- Creating Proc objects with Proc.new
- Callable objects and closures
Why It Matters
Blocks are Ruby's most distinctive feature and the foundation of its Iterator pattern. Every each, map, and select call uses a block. Durga Antivirus Pro uses blocks for file scanning callbacks and progress reporting. DodaZIP uses blocks for compression pipeline stages. Understanding blocks is essential for writing idiomatic Ruby.
Real-World Use
Rails uses blocks extensively: before_save { ... }, validates :name, presence: true (blockless), link_to "Home", root_path, class: "nav-link". Sinatra routes: get '/hello' do ... end. Every Ruby developer encounters blocks daily.
flowchart LR
A["Blocks & Procs"] --> B["Yield"]
B --> C["Block Arguments"]
C --> D["&block"]
D --> E["Proc Objects"]
E --> F["Lambdas"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Blocks
A block is a chunk of code between do...end or { }:
# do...end style (for multi-line)
[1, 2, 3].each do |n|
puts n * 2
end
# { } style (for single-line)
[1, 2, 3].each { |n| puts n * 2 }
Using Yield
yield calls the block passed to the method:
def greet
puts "Before yield"
yield
puts "After yield"
end
greet { puts "Hello from block" }
# Before yield
# Hello from block
# After yield
Yielding with Arguments
def calculate(a, b)
result = yield(a, b)
"Result: #{result}"
end
puts calculate(5, 3) { |x, y| x + y }
# Result: 8
puts calculate(5, 3) { |x, y| x * y }
# Result: 15
Checking If a Block Was Given
def greet
if block_given?
yield
else
puts "No block provided"
end
end
greet { puts "Hello" } # Hello
greet # No block provided
The &block Parameter
Capture a block as a named Proc parameter using &:
def repeat(times, &block)
times.times { block.call }
end
repeat(3) { puts "Hello" }
# Hello
# Hello
# Hello
Converting Between Blocks and Procs
# Block to Proc with &
def run_proc(&my_block)
puts "Running block: #{my_block.call}"
end
run_proc { "Hello from block" }
# Running block: Hello from block
# Proc to Block with &
my_proc = Proc.new { "Hello from proc" }
[1, 2, 3].each(&my_proc) # No output (each doesn't print)
Proc Objects
A Proc is a block turned into an object:
# Creating a Proc
my_proc = Proc.new { |name| "Hello, #{name}!" }
another_proc = proc { |name| "Hi, #{name}!" } # Kernel#proc
# Calling a Proc
puts my_proc.call("Alice") # Hello, Alice!
puts my_proc["Bob"] # Hello, Bob!
puts my_proc.("Charlie") # Hello, Charlie!
puts my_proc === "Dave" # Hello, Dave!
Proc and Return
def test_proc
my_proc = Proc.new { return "from proc" }
my_proc.call
"from method" # Never reached
end
puts test_proc # from proc
Procs return from the enclosing method! This is different from lambdas.
Proc with Each
double = Proc.new { |n| n * 2 }
puts [1, 2, 3].map(&double).inspect
# [2, 4, 6]
Block as a Closure
Blocks capture the surrounding scope:
def make_counter
count = 0
Proc.new { count += 1 }
end
counter = make_counter
puts counter.call # 1
puts counter.call # 2
puts counter.call # 3
another_counter = make_counter
puts another_counter.call # 1 (independent)
Symbol to Proc
A common Ruby idiom is converting a symbol to a proc:
names = ["alice", "bob", "charlie"]
# Long form
puts names.map { |name| name.upcase }.inspect
# ["ALICE", "BOB", "CHARLIE"]
# Short form with Symbol#to_proc
puts names.map(&:upcase).inspect
# ["ALICE", "BOB", "CHARLIE"]
# Works with any method
puts names.map(&:length).inspect
# [5, 3, 7]
puts names.map(&:reverse).inspect
# ["ecila", "bob", "eirahlc"]
Passing Multiple Blocks
Technically, you can only pass one block per method call, but you can pass additional procs as regular arguments:
def process(data, success_proc, error_proc)
if data.valid?
success_proc.call(data)
else
error_proc.call(data)
end
end
success = Proc.new { |d| puts "Processed #{d}" }
error = Proc.new { |d| puts "Error with #{d}" }
data = "some data"
process(data, success, error)
The tap Method
Kernel#tap yields the receiver to a block and returns the receiver:
result = [1, 2, 3].map { |n| n * 2 }
.tap { |arr| puts "Mapped: #{arr.inspect}" }
.select { |n| n > 3 }
.tap { |arr| puts "Selected: #{arr.inspect}" }
# Mapped: [2, 4, 6]
# Selected: [4, 6]
Common Patterns
Iterator with Custom Logic
def filter_and_transform(array)
result = []
array.each do |item|
if yield(item) # Custom filter condition
result << item
end
end
result
end
numbers = [1, 2, 3, 4, 5, 6]
even_squares = filter_and_transform(numbers) { |n| n.even? }
puts even_squares.inspect # [2, 4, 6]
Resource Management
def with_file(filename)
file = File.open(filename, "r")
yield(file)
ensure
file.close if file
end
with_file("data.txt") do |file|
puts file.read
end
Common Mistakes
1. Forgetting to Check block_given?
def greet
yield # RuntimeError if no block given!
end
def greet
yield if block_given? # Safe
end
2. Confusing Proc Return Behavior
def test
p = Proc.new { return "from proc" }
q = lambda { return "from lambda" }
p.call # Returns from test method!
q.call # Returns from lambda only
"from method"
end
puts test # from proc (lambda version would return "from method")
3. Using puts vs Returning from Blocks
# Block that returns value (correct for map)
[1, 2, 3].map { |n| n * 2 }
# Block that prints (correct for each)
[1, 2, 3].each { |n| puts n }
4. Forgetting & When Passing Proc to Method
my_proc = Proc.new { "hello" }
def run_block(&block)
block.call
end
run_block(my_proc) # WRONG — ArgumentError (expected block, got Proc)
run_block(&my_proc) # RIGHT — & converts Proc to block
5. Overusing Complex Block Chains
# Hard to read
data.map { |x| x.strip }
.select { |x| x.length > 0 }
.map { |x| x.capitalize }
.each { |x| puts x }
# Better with intermediate variables
stripped = data.map(&:strip)
non_empty = stripped.select { |x| x.length > 0 }
capitalized = non_empty.map(&:capitalize)
capitalized.each { |x| puts x }
Practice Questions
1. What does yield do in a Ruby method?
yield calls the block that was passed to the method. It can pass arguments to the block and can receive the block's return value.
2. How do you convert a block to a Proc object?
Add &block as the last parameter in the method definition. This captures the block as a Proc object. To convert a Proc to a block, use &proc_obj when calling a method.
3. What's the difference between Proc.new and lambda?
Lambdas check argument count (arity) and return from the lambda itself. Procs don't check arity and return from the enclosing method. Use lambdas when you want function-like behavior.
4. How does Symbol#to_proc work?
:upcase.to_proc creates a Proc that calls :upcase on its argument. ["hello"].map(&:upcase) is equivalent to ["hello"].map { |s| s.upcase }.
Challenge: Create a measure_time method that takes a block, runs it, and prints the execution time. Return the block's return value.
Solution
def measure_time(&block)
start = Time.now
result = block.call
elapsed = Time.now - start
puts "Completed in #{elapsed.round(4)}s"
result
end
result = measure_time do
sleep 0.5
"Done"
end
puts result # Done
Expected output:
Completed in 0.5001s
Done
FAQ
{{< faq question="What's the difference between |n| and &block?" >}}
|n| is a block parameter — it receives yielded values inside the block. &block captures the entire block as a Proc. The | | syntax receives data, & captures code.
{{< /faq >}}
{{< faq question="Can I yield multiple times?" >}}
Yes. A method can yield multiple times, passing different arguments each time. This is how iterators work — each yields each element in sequence.
{{< /faq >}}
{{< faq question="What's the difference between do...end and { }?" >}}
They're functionally identical. Convention: use do...end for multi-line blocks and { } for single-line blocks. There's a subtle precedence difference — { } binds tighter than do...end.
{{< /faq >}}
{{< faq question="Can I pass multiple blocks to a method?" >}}
No, Ruby methods accept exactly one block. However, you can pass additional procs as regular arguments: my_method(arg1, proc1, proc2) { ... }.
{{< /faq >}}
{{< faq question="What is a closure in Ruby?" >}} A closure is a block or proc that captures the surrounding variables at the time of creation, even if those variables go out of scope. This enables patterns like counters, iterators, and callbacks. {{< /faq >}}
Try It Yourself
# blocks_demo.rb
def timer
start = Time.now
result = yield if block_given?
puts "Took #{(Time.now - start).round(4)}s"
result
end
# Measure sleep
timer { sleep 0.3 }
# Create a counter closure
def make_counter(start = 0)
count = start
Proc.new { count += 1 }
end
counter_a = make_counter(10)
counter_b = make_counter(100)
puts "A: #{counter_a.call}, #{counter_a.call}"
puts "B: #{counter_b.call}"
# Symbol to proc
words = %w[ruby blocks procs lambda]
puts "Uppercase: #{words.map(&:upcase).join(', ')}"
puts "Lengths: #{words.map(&:length).inspect}"
Expected output:
Took 0.3001s
A: 11, 12
B: 101
Uppercase: RUBY, BLOCKS, PROCS, LAMBDA
Lengths: [4, 6, 5, 6]
What's Next
Now that you understand blocks and procs, learn about lambdas — stricter, function-like closures that differ from procs in argument handling and return behavior.
| Topic | Description | Link |
|---|---|---|
| Ruby Lambdas | ->, lambda, arity, closure | {{< ref "13-lambdas" >}} |
| Ruby Mixins | Comparable, Enumerable | {{< ref "14-mixins" >}} |
| Python Lambda Functions | Compare Python lambda expressions | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro