Blocks, Procs, and Lambdas in Ruby – Complete Beginner’s Guide

Blocks, Procs, and Lambdas in Ruby – Complete Beginner’s Guide

📦 What is a Block in Ruby?

A block in Ruby is a piece of code that is enclosed between either do...end or curly braces {...}. It’s like an **anonymous function** that can be passed into a method as an argument.

Blocks are **not objects themselves**, but they can be converted into Procs, which are objects. They are very commonly used in methods like each, map, and select to define what action to perform on elements.

A block can accept parameters and contains logic that runs when a method yields to it. A method can call the block using the yield keyword or by accepting an explicit &block argument.

# Using a block with do...end
    3.times do |i|
      puts \"Block called with i = #{i}\"
    end
    
    # Using a block with {}
    3.times { |i| puts \"Short block for i = #{i}\" }
    
Remember: A block is not an object by default, but it can be converted into a Proc using &block.
  • Not assigned to a variable directly (unlike Procs or Lambdas)
  • Automatically passed to methods that expect a block
  • Uses yield or &block.call to execute

In essence, blocks are a **core part of Ruby’s elegant syntax for handling iteration, callbacks, and closures**.

🔤 Syntax: do...end vs {} in Ruby Blocks

Ruby allows two syntaxes for writing blocks: do...end and {...}. Both achieve the same purpose — they define a block of code — but their usage and readability differ slightly based on context and convention.

# do...end is preferred for multi-line blocks
    [1, 2, 3].each do |n|
      puts \"Number: #{n}\"
    end
    
    # {} is preferred for single-line blocks
    [1, 2, 3].each { |n| puts \"Number: #{n}\" }
    

While these are functionally equivalent, Ruby developers follow a convention:

  • do...end is typically used for multi-line blocks to enhance readability.
  • {} is typically used for single-line blocks to keep code concise.
Tip: Precedence matters! Use parentheses to avoid unexpected behavior with `{}` inside method arguments.
# Ambiguity without parentheses
    puts [1, 2, 3].map { |n| n * 2 } # Good
    puts ([1, 2, 3].map { |n| n * 2 }) # Safer
    

🧲 How yield Works with Blocks in Ruby

The yield keyword in Ruby is used to call a block that has been passed to a method. It allows a method to temporarily transfer control to the block, execute the code inside it, and then return back to the method.

You can think of yield as a placeholder for the block. When the method runs and hits yield, it executes the block’s content.

# Example with yield
    def wrapper
      puts \"Before yield\"
      yield
      puts \"After yield\"
    end
    
    wrapper { puts \"Inside the block\" }
    
    # Output:
    # Before yield
    # Inside the block
    # After yield
    

You can also pass arguments to yield that the block will receive:

def greet
      yield(\"Alice\")
    end
    
    greet { |name| puts \"Hello, #{name}!\" }
    
    # Output:
    # Hello, Alice!
    
  • No explicit block parameter is needed when using yield.
  • If no block is given and yield is called, Ruby raises a LocalJumpError.
Tip: Use block_given? to check if a block was passed to avoid runtime errors.
def maybe_yield
      if block_given?
        yield
      else
        puts \"No block provided.\"
      end
    end
    

🔧 What is a Proc in Ruby?

A Proc (short for Procedure) is an object in Ruby that holds a block of code. Unlike blocks, which are not objects, a Proc can be stored in a variable, passed around, and called multiple times.

Procs allow you to encapsulate logic in a reusable object and delay execution until you explicitly invoke it with .call.

# Creating a Proc
    say_hello = Proc.new { puts "Hello from a Proc!" }
    
    # Calling a Proc
    say_hello.call
    
    # Output:
    # Hello from a Proc!
    
  • Procs are instances of the Proc class.
  • They can accept parameters like methods.
  • They remember the scope in which they were defined (closure).
# Proc with parameters
    greet = Proc.new { |name| puts "Hi, #{name}!" }
    greet.call("Ruby") # Output: Hi, Ruby!
    
Good to Know: You can pass Procs to methods using the & operator, just like a block!
def run(proc)
      proc.call
    end
    
    my_proc = Proc.new { puts "Running via method" }
    run(my_proc)
    

🛠️ Creating and Calling a Proc in Ruby

In Ruby, there are several ways to create a Proc. Once created, a Proc can be called using the .call method or simply with square brackets [].

📌 1. Using Proc.new

say_hi = Proc.new { puts "Hi there!" }
    say_hi.call    # Output: Hi there!
    say_hi[]       # Alternative way to call

📌 2. Using proc keyword

say_hello = proc { puts "Hello!" }
    say_hello.call   # Output: Hello!

📌 3. With parameters

greet = Proc.new { |name| puts "Welcome, #{name}!" }
    greet.call("Ruby")  # Output: Welcome, Ruby!
Tip: Procs do not strictly enforce the number of arguments, unlike Lambdas.
flex_proc = Proc.new { |x, y| puts "x=#{x}, y=#{y}" }
    flex_proc.call(1)  # Output: x=1, y=

You can pass a Proc to a method using the & operator, which turns it into a block:

def do_twice(&block)
      block.call
      block.call
    end
    
    repeat = Proc.new { puts "Run me!" }
    do_twice(&repeat)
    
    # Output:
    # Run me!
    # Run me!
    

🔐 What is a Lambda in Ruby?

A Lambda in Ruby is a special kind of Proc object that behaves more like a method. It is used to store code that you want to execute later, just like a Proc, but with a few key differences:

  • It checks the number of arguments strictly (like a method).
  • return behaves differently — it exits only from the lambda, not the enclosing method.

Lambdas are often used when you need reusable logic with well-defined input validation and safe return behavior.

📌 Creating a Lambda

# Using the lambda keyword
    greet = lambda { |name| puts "Hello, #{name}!" }
    
    # Using the stabby arrow syntax
    shout = ->(word) { puts word.upcase }
    
    greet.call("Ruby")   # Output: Hello, Ruby!
    shout.call("lambda") # Output: LAMBDA
    
Remember: Lambdas are stricter than Procs. Missing or extra arguments will raise an error.
# Lambda with 2 parameters
    add = ->(a, b) { a + b }
    
    add.call(2, 3)     # ✅ Works
    add.call(1)        # ❌ ArgumentError
    

Behind the scenes, Lambdas are also instances of the Proc class:

puts greet.class  # => Proc
    puts shout.class  # => Proc
    

⚙️ Creating and Calling a Lambda in Ruby

In Ruby, lambdas can be created using either the lambda keyword or the concise “stabby” lambda syntax (->). Once created, you can call them using .call, [], or .().

🔹 1. Using lambda keyword

say_hello = lambda { puts "Hello from lambda!" }
    say_hello.call      # Output: Hello from lambda!
    say_hello[]         # Also works
    say_hello.()        # Also works

🔹 2. Using -> (stabby lambda)

double = ->(n) { n * 2 }
    puts double.call(5)   # Output: 10

🔹 3. Strict argument checking

Lambdas behave like methods — they raise an error if you provide the wrong number of arguments:

triple = ->(x) { x * 3 }
    
    triple.call(3)   # ✅ Output: 9
    triple.call      # ❌ ArgumentError: wrong number of arguments (given 0, expected 1)

🔹 4. Lambdas return control cleanly

def lambda_test
      lam = -> { return "from lambda" }
      lam.call
      return "after lambda"
    end
    
    puts lambda_test
    # Output: "after lambda"
    
Key Point: Lambdas don’t exit the enclosing method — unlike Procs, which do.

⚖️ Block vs Proc vs Lambda – Key Differences

While all three — blocks, Procs, and lambdas — deal with passing and storing code in Ruby, they have crucial differences in terms of behavior, object orientation, and control flow.

FeatureBlockProcLambda
Is it an object?❌ No✅ Yes (Proc class)✅ Yes (Proc subclass)
How it’s createddo...end or {}Proc.new or proclambda or ->
Strict argument checking❌ No❌ No✅ Yes
Return behaviorReturns from methodReturns from methodReturns only from lambda
Reusable?❌ No✅ Yes✅ Yes
Summary: Use blocks for inline behavior, procs for flexible callbacks, and lambdas for method-like precision with argument checking.

🔄 Return Behavior: Proc vs Lambda

One of the most critical differences between a Proc and a Lambda in Ruby is how the return keyword behaves when used inside them. Understanding this helps you avoid unexpected control flow issues.

📌 Behavior of return in Proc

If you use return inside a Proc, it will try to return from the enclosing method where the Proc was defined — even if that wasn’t your intention.

def test_proc
      prc = Proc.new { return "from proc" }
      prc.call
      return "after proc"
    end
    
    puts test_proc   # Output: "from proc"
    

💥 As you can see, the return inside the Proc exits the entire method early.

📌 Behavior of return in Lambda

In contrast, a Lambda behaves like a method — return exits only from the Lambda itself, not the surrounding method.

def test_lambda
      lam = -> { return "from lambda" }
      lam.call
      return "after lambda"
    end
    
    puts test_lambda   # Output: "after lambda"
    
Key Takeaway: Use Lambdas if you want return to behave safely like inside a method. Avoid return in Procs unless you’re sure about the flow.

🧮 Arity (Argument Checking) Differences in Block, Proc, and Lambda

Arity refers to the number of arguments a code block, proc, or lambda expects. Ruby treats arity differently depending on whether you’re using a block, a Proc, or a lambda.

🔹 Blocks and Procs: Loose Argument Checking

Procs and blocks are lenient — they don’t raise errors if the wrong number of arguments are passed.

my_proc = Proc.new { |x, y| puts "x=#{x}, y=#{y}" }
    
    my_proc.call(1, 2)     # Output: x=1, y=2
    my_proc.call(1)        # Output: x=1, y=
    my_proc.call           # Output: x=, y=
    

🔹 Lambda: Strict Argument Checking

Lambdas behave like methods — if you pass too many or too few arguments, Ruby raises an error.

my_lambda = ->(x, y) { puts "x=#{x}, y=#{y}" }
    
    my_lambda.call(1, 2)   # ✅ Output: x=1, y=2
    my_lambda.call(1)      # ❌ ArgumentError
    my_lambda.call         # ❌ ArgumentError
    
Summary: Use lambdas when exact arguments matter. Use procs or blocks when flexibility is acceptable or desired.

🔄 Use of &block to Convert Block to Proc

In Ruby, the &block syntax in method definitions is a way to capture an implicit block and convert it into an explicit Proc object. This allows you to store, pass, or call the block later.

🔹 How it works

def greet(&block)
      puts "Before block"
      block.call if block
      puts "After block"
    end
    
    greet { puts "Hello from block!" }
    # Output:
    # Before block
    # Hello from block!
    # After block
    

🔹 Passing block as Proc to another method

You can also forward a block to another method using the & operator:

def execute(&block)
      do_something(&block)
    end
    
    def do_something
      yield
    end
    
    execute { puts "Forwarded block!" }
    

🔹 Convert a Proc to a block

You can also convert a Proc into a block by prefixing it with &:

my_proc = Proc.new { puts "Proc to block!" }
    
    def call_block(&block)
      block.call
    end
    
    call_block(&my_proc)
    
Tip: Use &block when you want to reuse a block multiple times or pass it around explicitly like any other variable.

🧠 When to Use Block, Proc, or Lambda

Choosing between a block, proc, or lambda depends on what behavior you need — inline execution, reusable logic, or strict method-like functions.

🔹 Use a Block when:

  • You’re writing simple, one-off behavior (like iterating).
  • You want implicit behavior with methods like each, map, or select.
  • You don’t need to reuse the logic elsewhere.
[1, 2, 3].each do |n|
      puts n * 2
    end
    

🔹 Use a Proc when:

  • You want to store code in a variable and reuse it.
  • You want flexible argument behavior (loose arity).
  • You’re passing behavior into multiple methods or callbacks.
double = Proc.new { |n| n * 2 }
    puts double.call(5)         # 10
    puts double.call            # nil
    

🔹 Use a Lambda when:

  • You want a method-like function with strict argument checking.
  • You want predictable return behavior (exits only from lambda).
  • You need to define small reusable functions (e.g., filters).
square = ->(x) { x ** 2 }
    puts square.call(4)         # 16
    puts square.call            # ArgumentError
    

🧪 10 Real Use Case Examples

  1. Block: Custom logger
    def with_logging
          puts "Start"
          yield
          puts "End"
        end
        
        with_logging { puts "Doing work..." }
        
  2. Block: Wrapping database transactions
    ActiveRecord::Base.transaction do
          # multiple DB operations
        end
        
  3. Proc: Callback reuse
    callback = Proc.new { puts "Running callback!" }
        
        3.times(&callback)
        
  4. Proc: Optional behavior
    def maybe(&block)
          block.call if block
        end
        
        maybe { puts "Only run if block passed" }
        
  5. Lambda: Functional filtering
    filter = ->(x) { x > 5 }
        puts [2, 4, 6, 8].select(&filter)   # [6, 8]
        
  6. Lambda: Data validation
    check_name = ->(name) {
          raise "Too short!" if name.length < 3
        }
        
        check_name.call("Jo")   # Raises error
        
  7. Proc: Retry logic wrapper
    retryable = Proc.new do
          puts "Trying..."
          raise if rand > 0.5
        end
        
        3.times { retryable.call rescue puts "Failed, retrying..." }
        
  8. Block: Benchmarking performance
    def benchmark
          start = Time.now
          yield
          puts "Time: #{Time.now - start}"
        end
        
        benchmark { sleep(1) }
        
  9. Lambda: Custom tax calculator
    calculate_tax = ->(amount) { amount * 0.13 }
        
        puts calculate_tax.call(1000)   # 130.0
        
  10. Proc: Pass behavior to a method
    def apply_twice(proc)
          proc.call
          proc.call
        end
        
        say_hi = Proc.new { puts "Hi!" }
        apply_twice(say_hi)
        
Recommendation: Use block for inline operations, proc for reusable or flexible behaviors, and lambda for strict control like methods.

⚠️ Common Mistakes and Gotchas in Blocks, Procs, and Lambdas

Ruby offers flexibility with blocks, procs, and lambdas — but that also leads to confusing behavior if you're not careful. Here are the most common pitfalls you should avoid:

1. ❌ Assuming Proc and Lambda Behave the Same

They look similar but behave very differently:

  • Proc has loose argument checking
  • Lambda is strict about arity
p = Proc.new { |x, y| puts "#{x}, #{y}" }
    p.call(1)        # No error, prints: 1,
    
    l = ->(x, y) { puts "#{x}, #{y}" }
    l.call(1)        # ArgumentError!
    

2. ❌ Unexpected Return Behavior

Returning from a proc exits the enclosing method. Returning from a lambda exits only the lambda:

def test_proc
      p = Proc.new { return "from proc" }
      p.call
      return "after proc"
    end
    
    puts test_proc   # => "from proc"
    
    def test_lambda
      l = -> { return "from lambda" }
      l.call
      return "after lambda"
    end
    
    puts test_lambda # => "after lambda"
    

3. ❌ Forgetting to Use yield with Blocks

Defining a block but not calling yield means the block won't run:

def greet
      puts "Hello"
      # yield is missing!
      puts "Goodbye"
    end
    
    greet { puts "Inside block" }
    # Output: Hello \n Goodbye
    

4. ❌ Not Checking if Block Exists

Calling yield without a block leads to an error. Use block_given? to prevent this:

def greet
      yield if block_given?
    end
    
    greet    # ✅ No error
    

5. ❌ Forgetting & When Passing Procs

When passing a Proc as a block, don’t forget the &:

my_proc = Proc.new { puts "Hello" }
    
    def run(&block)
      block.call
    end
    
    run(&my_proc)  # ✅ Correct
    run(my_proc)   # ❌ Error: wrong number of arguments
    
Pro Tip: Always test return behavior and argument flexibility when switching between procs and lambdas. What works for one might break in the other.

🧪 Block – 3 Case Studies

Blocks are one of the most idiomatic and powerful features in Ruby. They are widely used in iterators, custom method wrappers, and domain-specific languages (DSLs). Below are 3 real case studies showcasing how and why to use blocks in practical Ruby code.

🔹 Case Study 1: Iteration with Blocks

Description: Blocks are commonly passed to enumerator methods like each, map, and select.

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

Why: Cleaner and concise syntax for quick inline logic during iteration. Enhances readability for small operations.

🔹 Case Study 2: Custom Method Wrapping

Description: Blocks let you wrap logic dynamically inside methods. This is useful for things like logging, benchmarking, and error handling.

def with_logging
      puts "Started..."
      yield
      puts "Finished."
    end
    
    with_logging { puts "Running main logic..." }

Why: Reusable wrapper pattern without hardcoding behavior. Perfect for Rails callbacks or decorators.

🔹 Case Study 3: Building DSLs (Domain Specific Languages)

Description: Ruby gems like RSpec, Capybara, and Rails itself use blocks to allow expressive, readable syntax in DSLs.

describe "Calculator" do
      it "adds numbers" do
        expect(1 + 2).to eq(3)
      end
    end

Why: Blocks allow structured yet flexible control flows that make DSLs feel natural and clean to read and write.

Summary: Blocks are ideal for inline logic, dynamic behavior wrapping, and clean DSL syntax. They’re a foundational piece of expressive Ruby code.

🧪 Proc – 3 Case Studies

Procs allow code blocks to be stored in variables and reused, passed around, or deferred. This makes them great for decoupling logic and achieving clean abstraction. Here are three real-world examples:

🔹 Case Study 1: Storing and Reusing Logic

Description: Define a Proc once and reuse it wherever needed, reducing code duplication.

square = Proc.new { |n| n * n }
    puts square.call(3)  # => 9
    puts square.call(5)  # => 25
    

Why: Encapsulates logic into reusable objects, helpful when the same operation is needed in many places.

🔹 Case Study 2: Passing Logic to Methods

Description: Procs can be passed as arguments to methods using & to be invoked as blocks.

printer = Proc.new { |name| puts "Hello, #{name}" }
    
    def greet(&block)
      block.call("Ruby")
    end
    
    greet(&printer)
    

Why: Makes your method more dynamic — it can execute different logic based on the Proc passed in.

🔹 Case Study 3: Closures and Persistent Scope

Description: Procs remember the scope where they were defined — including local variables.

def multiplier(factor)
      Proc.new { |n| n * factor }
    end
    
    double = multiplier(2)
    puts double.call(5)  # => 10
    
    triple = multiplier(3)
    puts triple.call(5)  # => 15
    

Why: Enables creation of powerful closure-based utilities — similar to function factories in JavaScript or Python.

Summary: Procs let you treat code like data — store it, pass it, reuse it. Perfect for reusable logic and closures.

🧪 Lambda – 3 Case Studies

Lambdas are similar to Procs but stricter in behavior. They enforce argument count (arity) and return control only within themselves, making them safer and more predictable in many use cases. Below are three practical scenarios where lambdas shine.

🔹 Case Study 1: Safe Argument Checking

Description: Lambda will raise an error if the number of arguments passed does not match expected count.

add = ->(a, b) { a + b }
    puts add.call(2, 3)     # ✅ 5
    puts add.call(2)        # ❌ ArgumentError
    

Why: Better for methods that need strict contracts, especially in API logic or form validation pipelines.

🔹 Case Study 2: Returning from Within a Lambda

Description: Lambdas return only from themselves — they don’t break out of the method they’re in.

def run
      l = -> { return "from lambda" }
      l.call
      "after lambda"
    end
    
    puts run  # => "after lambda"
    

Why: Prevents accidental early return from the containing method, unlike Procs.

🔹 Case Study 3: Functional Pipelines

Description: Lambdas are perfect for chaining logic in functional-style programming.

capitalize = ->(s) { s.capitalize }
    add_suffix = ->(s) { "#{s}!" }
    
    def run_pipeline(str, *steps)
      steps.reduce(str) { |val, step| step.call(val) }
    end
    
    puts run_pipeline("hello", capitalize, add_suffix)  # => "Hello!"
    

Why: Predictable and composable — ideal for chained transformations and clean functional design.

Summary: Lambdas are strict and safe. Use them when you need predictable argument validation and safe return behavior inside complex flows.

💼 Interview Questions & Answers – Ruby Blocks, Procs, and Lambdas

Below are commonly asked interview questions that test your understanding of how blocks, procs, and lambdas work in Ruby. Each includes a detailed explanation and code example where applicable.

1. What is a block in Ruby?

A block is an anonymous piece of code passed to a method. It can be defined using do...end or { }. It's not an object, but it can be converted into one using & to become a Proc.

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

2. What's the difference between a Proc and a Lambda?

Both are objects of class Proc, but:

  • Lambdas enforce strict arity (argument count), Procs do not.
  • Lambdas return only from themselves, Procs return from the enclosing method.

3. How do you convert a block to a Proc?

You can use &block in method definitions to capture a block and treat it like a Proc object.

def greet(&block)
      block.call if block
    end

4. What is the return behavior difference between Proc and Lambda?

A Proc returns from the entire method it’s defined in, whereas a Lambda returns only from itself.

def demo
      p = Proc.new { return "From Proc" }
      p.call
      return "After Proc"
    end
    
    puts demo  # => "From Proc"

5. How does yield work in Ruby?

yield is used to execute a block passed to a method. If no block is passed, calling yield raises an error unless handled.

def test
      yield if block_given?
    end

6. Can you pass multiple blocks to a method?

No, Ruby allows only one implicit block per method. However, you can simulate multiple blocks by passing multiple Procs or Lambdas.

7. How do you define a Lambda?

You can define it using -> or lambda keyword:

l = ->(x) { x * 2 }
    # or
    l = lambda { |x| x * 2 }

8. Can a Proc take variable arguments?

Yes. A Proc is lenient with arguments, and will not raise an error if arguments are missing or extra.

p = Proc.new { |a, b| puts "a=#{a}, b=#{b}" }
    p.call(1)       # b is nil
    p.call(1, 2, 3) # extra ignored

9. Why would you use a block over a Proc?

Blocks are more idiomatic and convenient for short, inline logic. Procs are better when you need to reuse logic or pass it between methods explicitly.

10. What is the difference between block_given? and &block?

block_given? checks if a block was passed to the method. &block captures the block into a Proc object for more flexible use.

def example(&block)
      puts "Has block: #{block_given?}"
      block.call if block
    end
Pro Tip: Practicing with code and observing behavior of return/arity is the best way to master this topic before interviews.

🌐 External Resources

Dive deeper into Ruby blocks, Procs, and Lambdas with these high-quality external references:

📌 Tip: Bookmark these resources for quick reference and revision during interviews or development.

Learn more about Rails setup

Scroll to Top