Ruby Blocks, Procs, And Lambdas Explained

Learn how Ruby blocks, Procs, and lambdas work, how they differ, and when to use each in real-world Ruby applications.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet Ruby on Rails Developer
Ruby Blocks, Procs, and Lambdas Explained

• 10 min read

Share with friends

If you have written Ruby for more than a few weeks, you have already used blocks. Every time you call each, map, or times with curly braces or a do...end, you are passing a block. But many developers use blocks every day without fully understanding what they are, how yield works under the hood, or how blocks relate to Procs and lambdas.

This article walks through blocks, Procs, and lambdas from the ground up. By the end, you will understand how each one works, how they differ, and when to reach for each one in real Ruby and Rails code.


What Is a Block in Ruby?

A block is a chunk of code that you can pass to a method. It is not an object on its own. It has no name, and you cannot store it in a variable. A block only exists as an argument to a method call.

You have likely written blocks like this:

[1, 2, 3].each do |number|
puts number
end

You can also write a block using curly braces:

[1, 2, 3].each { |number| puts number }

Both versions do the same thing. The convention in the Ruby community is to use do...end for multi-line blocks and curly braces for single-line blocks. This is not a syntax rule, just a widely followed style.

How Blocks Are Passed to Methods

Every method in Ruby can accept a block, whether or not it explicitly says so in its parameter list. The method decides whether to use the block or ignore it.

def greet(name)
puts "Hello, #{name}"
end

greet("Sam") { puts "This block is never used" }

The block above is silently ignored because greet never calls it. This is a common source of confusion for developers coming from languages where passing an unexpected argument raises an error. In Ruby, a block that is not used simply does nothing.


Working with yield

To actually use a block inside a method, you call yield. When Ruby hits yield, it pauses the method and executes the block, then returns control to the method afterward.

def greet(name)
puts "Before yield"
yield
puts "After yield"
end

greet("Sam") { puts "Hello, #{name}" if false }
greet("Sam") { puts "Hello from the block" }

Running the second call prints:

Before yield
Hello from the block
After yield

You can check whether a method was given a block at all using block_given?. This avoids raising a LocalJumpError when yield is called without a block present.

def process
if block_given?
yield
else
puts "No block was given"
end
end

process
process { puts "Doing work" }

Block Parameters vs Method Arguments

Block parameters look similar to method arguments, but they behave differently in one important way: blocks do not raise an error when you pass the wrong number of arguments.

def call_with_two
yield(1, 2)
end

call_with_two { |a| puts a }
call_with_two { |a, b, c| puts [a, b, c].inspect }

In the first call, the extra argument is simply dropped. In the second call, the missing argument becomes nil. Compare this to a regular method, which raises ArgumentError immediately if the argument count does not match.

This lenient behavior is intentional. It lets a single method work with blocks of varying arity, which is especially useful when iterating over structures like hashes:

{ a: 1, b: 2 }.each do |key, value|
puts "#{key} => #{value}"
end

{ a: 1, b: 2 }.each do |pair|
puts pair.inspect
end

Both blocks above work on the same each call. Ruby adapts the yielded values to match how many parameters the block declares.


Turning a Block into a Proc with &block

Sometimes you want to capture a block as a named object instead of just yielding to it. You do this by adding a parameter prefixed with & to your method definition.

def run_it(&block)
puts block.class
block.call
end

run_it { puts "Running the captured block" }

The & symbol converts the block into a Proc object and assigns it to the block parameter. Now you can pass it to other methods, store it, or call it multiple times.

You can also use & in the opposite direction, converting an existing Proc back into a block when calling a method:

square = proc { |n| n * n }
[1, 2, 3].map(&square)

This pattern shows up constantly with symbols too. &:upcase works because Symbol#to_proc converts the symbol into a Proc, which & then converts into a block.

["ruby", "rails"].map(&:upcase)

Understanding Procs

A Proc is an object that wraps a block of code so it can be stored, passed around, and called whenever you want. You create one with Proc.new or the shorthand proc.

say_hello = proc { |name| puts "Hello, #{name}" }
say_hello.call("Sam")
say_hello.("Sam")
say_hello["Sam"]

All three calling styles above do the same thing. .call, .(), and [] are interchangeable ways to invoke a Proc.

Procs are closures. This means they remember the variables from the scope where they were created, even after that scope has finished executing.

def counter
count = 0
increment = proc { count += 1 }
increment
end

add_one = counter
puts add_one.call
puts add_one.call
puts add_one.call

Each call increases count, and that state persists between calls because the Proc holds onto its original environment.

Proc Argument Handling

Like blocks, Procs are forgiving about argument count. Passing too few or too many arguments does not raise an error.

add = proc { |a, b| (a || 0) + (b || 0) }
puts add.call(1)
puts add.call(1, 2, 3)

Proc Return Behavior

This is where Procs get tricky. A return inside a Proc does not just exit the Proc. It attempts to return from the enclosing method, which can lead to unexpected behavior or errors if that method has already finished running.

def test_proc
my_proc = proc { return "Returned from proc" }
my_proc.call
puts "This line never runs"
end

test_proc

The puts statement above never executes because return inside the Proc exits test_proc entirely. This is one of the most common sources of confusing bugs when developers treat Procs and lambdas as interchangeable.


Understanding Lambdas

A lambda is also a Proc, but with two important differences: strict argument checking and different return behavior. You create a lambda with lambda or the -> stabby lambda syntax.

multiply = lambda { |a, b| a * b }
multiply2 = ->(a, b) { a * b }

puts multiply.call(2, 3)
puts multiply2.call(2, 3)

Lambda Argument Handling

Unlike a Proc, a lambda behaves like a regular method when it comes to arguments. Calling it with the wrong number raises ArgumentError.

strict = ->(a, b) { a + b }
strict.call(1)

This raises an error immediately, which makes lambdas safer to use when you want to catch mistakes early rather than silently getting nil values.

Lambda Return Behavior

A return inside a lambda only exits the lambda itself. It does not affect the method that called it.

def test_lambda
my_lambda = -> { return "Returned from lambda" }
result = my_lambda.call
puts "This line runs fine"
result
end

puts test_lambda

Both lines print here, unlike the Proc example above. This predictable return behavior is one of the main reasons lambdas are generally the safer default choice.


Blocks vs Procs vs Lambdas: A Direct Comparison

Feature

Block

Proc

Lambda

Is an object

No

Yes

Yes

Can be stored in a variable

No

Yes

Yes

Argument count enforcement

Lenient

Lenient

Strict

return behavior

N/A

Returns from enclosing method

Returns from the lambda only

Created with

do...end or {}

Proc.new or proc

lambda or ->

Checking type

block_given?

is_a?(Proc) returns true

is_a?(Proc) returns true, lambda? returns true

You can check whether a given Proc is actually a lambda using the lambda? method:

p1 = proc {}
l1 = lambda {}

puts p1.lambda?
puts l1.lambda?

When to Use Each One

Use a block when you want to pass a one-off chunk of behavior to a method, and you do not need to store or reuse it. This covers the vast majority of everyday Ruby code, including iteration, transformation, and custom DSLs.

Use a Proc when you need closure behavior but do not care about strict argument checking, and you understand the return behavior well enough to avoid surprises. Procs are less common in application code today and show up more often in metaprogramming or older codebases.

Use a lambda when you want a reusable, storable piece of behavior with predictable argument checking and return behavior. Lambdas are generally the better choice whenever you are choosing between a Proc and a lambda for new code, because their strictness catches bugs earlier.


Common Mistakes to Avoid

Assuming Procs and lambdas behave identically. They are both instances of the Proc class, but their return behavior is different enough to cause real bugs, especially the "silent method exit" caused by return inside a Proc.

Forgetting block_given? before calling yield. Calling yield in a method that was not given a block raises a LocalJumpError. Always guard it, or design the method to require a block explicitly.

Overusing &block when yield would do. If you only need to yield once and do not need to pass the block elsewhere, yield is simpler and slightly faster than capturing the block as a Proc first.

Relying on argument leniency in production code. Just because a block or Proc will not raise an error on a mismatched argument count does not mean it is doing what you expect. Silent nil values are harder to debug than a raised exception.


Blocks, Procs, and Lambdas in Real Ruby and Rails Code

These concepts are not just academic. They appear constantly in everyday Rails development.

Active Record scopes and query methods rely heavily on blocks:

User.where(active: true).each do |user|
UserMailer.welcome_email(user).deliver_later
end

Custom scopes often accept blocks or use yield internally when building more advanced query logic, which becomes especially relevant once you start optimizing Active Record queries for performance.

Callbacks and validations frequently use lambdas for concise, self-contained logic:

class Order < ApplicationRecord
validate :total_is_positive

private

def total_is_positive
errors.add(:total, "must be positive") unless total.positive?
end
end

Configuration blocks are everywhere in Rails initializers and gem setup, where a block is yielded a configuration object:

Rails.application.configure do |config|
config.active_record.database_selector = { delay: 2.seconds }
end

If you are working with database configuration in Rails, understanding how these configuration blocks interact with your database layer matters just as much as understanding the syntax itself. It is worth pairing this knowledge with a broader understanding of choosing between SQLite and PostgreSQL for your Rails application, especially since configuration patterns can differ depending on which database you are running in production.

Service objects, a common Rails pattern, often accept blocks or lambdas to allow callers to customize behavior without subclassing:

class ImportService
def initialize(&on_success)
@on_success = on_success
end

def run
# import logic here
@on_success.call if @on_success
end
end

ImportService.new { puts "Import finished" }.run

And if you are running Rails in production with SQLite, many of the tuning and configuration patterns you will touch involve exactly this kind of block-based configuration API, which is covered in more depth in this complete guide to optimizing SQLite for Rails 8 production.


Wrapping Up

Blocks, Procs, and lambdas are three related but distinct tools in Ruby. Blocks are the simplest and most common, used for one-off behavior passed directly into a method call. Procs add the ability to store and reuse that behavior as an object, with lenient argument handling and a return behavior that can surprise you. Lambdas refine the Proc concept further, adding strict argument checking and predictable return behavior that makes them a safer default for reusable code.

Once you understand how yield, &block, and Ruby's closures fit together, these three concepts stop feeling like separate topics and start feeling like variations on the same underlying idea: passing behavior around as data.

💌 Don’t miss out! Join my newsletter for web development tips, tutorials, and insights delivered straight to your inbox.

Thanks for reading & Happy coding! 🚀

Follow me on:

Code. Learn. Grow.

A friendly newsletter sharing dev tips, lessons, and wins from my journey.