Back to Scaling Ruby Applications guides

Getting Started with Ruby Blocks, Procs, and Lambdas

Stanley Ulili
Updated on September 9, 2025

Blocks, Procs, and lambdas are some of Ruby's most distinctive and powerful features. They bring functional programming ideas into Ruby's object-oriented design, helping you write code that is more expressive, adaptable, and reusable. Grasping these concepts is crucial for mastering Ruby and unlocking its full potential.

These tools allow you to treat executable code as data, facilitating highly flexible and customizable behaviors. They are prevalent in Ruby, ranging from simple loops to sophisticated metaprogramming techniques employed in frameworks like Rails. Understanding them will transform how you approach problems and help you recognize the elegant patterns that make Ruby code so readable and maintainable.

In this tutorial, you'll learn how to harness these powerful tools to write more elegant and functional Ruby code.

Prerequisites

This guide assumes familiarity with basic Ruby syntax, methods, and classes. While examples use Ruby 2.7 and later, most concepts also apply to earlier versions.

Understanding Ruby blocks

Blocks are the foundation of Ruby's functional programming capabilities. They represent chunks of executable code that can be passed to methods, creating a bridge between imperative and functional programming styles. This flexibility is what makes Ruby's iteration methods so elegant and powerful.

To get started with Ruby's powerful callable objects, let's create a project directory to organize our examples:

 
mkdir ruby-blocks-tutorial && cd ruby-blocks-tutorial

Let's start with the fundamentals:

basic_blocks.rb
# Block with each method
numbers = [1, 2, 3, 4, 5]
numbers.each { |num| puts num * 2 }

puts "---"

# Multi-line block syntax
numbers.each do |num|
  squared = num * num
  puts "#{num} squared is #{squared}"
end

This example shows both single-line { } and multi-line do...end block syntax. The conventional rule is to use curly braces for single-line blocks and do...end for multi-line blocks, though both work identically. The choice often comes down to readability and team conventions.

 
ruby basic_blocks.rb
Output
2
4
6
8
10
---
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25

The output demonstrates how blocks receive arguments (the |num| part) and execute code for each element. The block parameter receives each array element in turn, allowing you to perform operations on each item without explicitly writing loops.

Now that you understand basic block syntax, let's explore how to create methods that can accept and use blocks in your own code.

Creating methods that accept blocks

Writing methods that accept blocks is where Ruby's power truly shines. This capability allows you to create highly reusable methods that can be customized with different behaviors at call time, following the principle of separation of concerns.

yield_examples.rb
def greet_users
  puts "Starting greetings..."
  yield "Alice"
  yield "Bob"
  yield "Charlie"
  puts "Finished greetings!"
end

def conditional_yield
  puts "This always runs"
  yield if block_given?
  puts "This also always runs"
end

# Using the methods
greet_users { |name| puts "Hello, #{name}!" }

puts "---"

conditional_yield { puts "Block was provided!" }
conditional_yield  # No block provided

The yield keyword executes the block passed to a method, while block_given? lets you check whether a block was provided. This pattern allows you to write methods that are flexible - they can work both with and without blocks, making your APIs more user-friendly.

 
ruby yield_examples.rb
Output
Starting greetings...
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Finished greetings!
---
This always runs
Block was provided!
This also always runs
This always runs
This also always runs

The output shows how yield passes values to the block and how block_given? prevents errors when no block is provided. This defensive programming approach ensures your methods are robust and won't crash when called without blocks.

Blocks become even more powerful when they can handle multiple parameters and return values to influence the calling method's behavior.

Working with block parameters

Blocks can accept multiple parameters and return values, creating sophisticated interfaces for data processing and transformation:

block_parameters.rb
def process_pairs
  yield 1, "one"
  yield 2, "two"  
  yield 3, "three"
end

def flexible_processor(items)
  items.each_with_index do |item, index|
    result = yield item, index
    puts "Result: #{result}"
  end
end

# Multiple parameters
process_pairs { |num, word| puts "#{num}: #{word}" }

puts "---"

# Block returning values
fruits = ["apple", "banana", "cherry"]
flexible_processor(fruits) do |fruit, index|
  "#{index + 1}. #{fruit.upcase}"
end

This pattern is fundamental to Ruby's design philosophy. Blocks can receive multiple arguments and return values back to the calling method, enabling the method to make decisions or modify behavior based on the block's output. This creates a powerful collaboration between the method's structure and the block's custom logic.

 
ruby block_parameters.rb
Output
1: one
2: two
3: three
---
Result: 1. APPLE
Result: 2. BANANA
Result: 3. CHERRY

The output demonstrates how blocks can process multiple parameters and return transformed values to the calling method. This pattern underlies many of Ruby's most useful methods like map, select, and reduce.

While blocks are powerful, they have limitations - they can't be stored in variables or passed around easily. This is where Procs come in.

Introduction to procs

Procs bridge the gap between blocks and objects by wrapping executable code in an object that can be stored, passed around, and reused. Think of Procs as "blocks with persistence" - they capture all the power of blocks while adding the flexibility of objects.

basic_procs.rb
# Creating Procs
square_proc = Proc.new { |x| x * x }
greeting_proc = proc { |name| "Hello, #{name}!" }

# Using Procs
puts square_proc.call(5)
puts greeting_proc.call("Ruby")

# Procs can be stored and reused
numbers = [1, 2, 3, 4, 5]
squared_numbers = numbers.map(&square_proc)
puts "Squared: #{squared_numbers}"

# Passing Procs to methods
def apply_operation(items, operation)
  items.map { |item| operation.call(item) }
end

doubled = apply_operation([1, 2, 3], proc { |x| x * 2 })
puts "Doubled: #{doubled}"

The & operator is crucial here - it converts a Proc to a block when passing it to methods expecting blocks. This allows you to reuse the same logic across multiple method calls, promoting code reuse and reducing duplication.

 
ruby basic_procs.rb
Output
25
Hello, Ruby!
Squared: [1, 4, 9, 16, 25]
Doubled: [2, 4, 6]

The output shows how Procs can be called multiple times and converted to blocks using the & operator for use with methods like map. This flexibility makes Procs ideal for storing reusable pieces of logic.

Ruby offers another callable object that's similar to Procs but with more strict behavior: lambdas.

Understanding lambdas

Lambdas are Ruby's attempt to bring more traditional function-like behavior to the language. While similar to Procs, they enforce stricter rules around arguments and return statements, making them more predictable in certain contexts.

basic_lambdas.rb
# Creating lambdas
square_lambda = lambda { |x| x * x }
arrow_lambda = ->(name) { "Hello, #{name}!" }

puts square_lambda.call(4)
puts arrow_lambda.call("Lambda")

# Lambdas are stricter about arguments
begin
  square_lambda.call(1, 2)  # Too many arguments
rescue ArgumentError => e
  puts "Lambda error: #{e.message}"
end

# Procs are more lenient
square_proc = Proc.new { |x| x * x }
puts "Proc with extra args: #{square_proc.call(3, 4, 5)}"  # Ignores extras

# Different return behavior
def test_returns
  my_lambda = -> { return "lambda return" }
  my_proc = Proc.new { return "proc return" }

  result = my_lambda.call
  puts "After lambda: #{result}"

  # This would exit the method entirely:
  # my_proc.call
  puts "This line executes"
end

test_returns

Lambdas enforce argument count strictly and have different return semantics than Procs. A return in a lambda returns from the lambda itself, while a return in a Proc returns from the enclosing method. This makes lambdas safer for certain use cases where you need predictable behavior.

 
ruby basic_lambdas.rb
Output
16
Hello, Lambda!
Lambda error: wrong number of arguments (given 2, expected 1)
Proc with extra args: 9
After lambda: lambda return
This line executes

The output demonstrates lambda's strict argument checking and how return statements behave differently compared to Procs. Choose lambdas when you need strict argument validation and predictable return behavior.

The real power of all these constructs lies in their ability to capture and remember the environment where they were created.

Closures and variable scope

One of the most powerful aspects of blocks, Procs, and lambdas is their ability to capture variables from their surrounding scope, creating closures. This feature enables sophisticated patterns like factories, counters, and configuration objects that "remember" their creation context.

closures.rb
def create_counter(start_value)
  current = start_value

  # This Proc captures the 'current' variable
  Proc.new do
    current += 1
    current
  end
end

def create_multiplier(factor)
  lambda { |x| x * factor }
end

# Using closures
counter = create_counter(10)
puts "First call: #{counter.call}"
puts "Second call: #{counter.call}"
puts "Third call: #{counter.call}"

puts "---"

double = create_multiplier(2)
triple = create_multiplier(3)

puts "Double 5: #{double.call(5)}"
puts "Triple 5: #{triple.call(5)}"

# Each closure maintains its own copy of captured variables
another_counter = create_counter(100)
puts "New counter: #{another_counter.call}"
puts "Old counter: #{counter.call}"

This demonstrates closures - the ability for blocks, Procs, and lambdas to "close over" variables from their defining scope. Each closure maintains its own copy of the captured variables, creating powerful patterns for state management and factory functions without needing classes.

 
ruby closures.rb
Output
First call: 11
Second call: 12
Third call: 13
---
Double 5: 10
Triple 5: 15
New counter: 101
Old counter: 14

The output shows how each closure maintains its own state independently, with the current variable persisting between calls and each multiplier remembering its own factor.

This behavior enables elegant solutions for problems that might otherwise require complex object hierarchies.

Final thoughts

Blocks, Procs, and lambdas are fundamental to Ruby's expressiveness and power. They enable functional programming patterns, create clean DSLs, and provide flexible interfaces for method design. Key takeaways include:

  • Blocks are code chunks passed to methods; use yield to execute them
  • Procs are objects containing blocks; they're lenient with arguments and return behavior
  • Lambdas are strict Procs that enforce argument counts and have different return semantics
  • Closures allow these constructs to capture and remember their creation environment
  • Choose the right tool: blocks for simple iteration, Procs for reusable code objects, lambdas for strict function-like behavior

Mastering these constructs will significantly improve your Ruby programming skills and help you understand the elegant patterns used throughout Ruby libraries and frameworks. Practice with these concepts and explore how popular gems use them to create powerful, flexible APIs.

Got an article suggestion? Let us know
Next article
Top 10 Ruby on Rails Alternatives for Web Development
Discover the top 10 Ruby on Rails alternatives. Compare Django, Laravel, Express.js, Spring Boot & more with detailed pros, cons & features.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.