0
0
Rubyprogramming~15 mins

Block syntax (do..end and curly braces) in Ruby - Deep Dive

Choose your learning style9 modes available
Overview - Block syntax (do..end and curly braces)
What is it?
In Ruby, blocks are chunks of code that you can pass to methods to be executed later. They are written using either do..end or curly braces {}. Blocks let you write flexible and reusable code by allowing methods to run custom code you provide.
Why it matters
Blocks solve the problem of repeating code patterns by letting you pass behavior as a piece of code. Without blocks, Ruby methods would be less flexible and you would write more repetitive code. Blocks make Ruby powerful for tasks like iteration, callbacks, and resource management.
Where it fits
Before learning blocks, you should understand Ruby methods and how to call them. After blocks, you can learn about Procs and lambdas, which are objects that hold blocks, and then explore advanced topics like enumerators and fibers.
Mental Model
Core Idea
A block is a little package of code you hand to a method to run inside it, written with do..end or curly braces.
Think of it like...
It's like giving a recipe card (the block) to a chef (the method) who follows your instructions exactly when cooking your meal.
Method call with block:

  method_name { |param| 
    # block code here
  }

or

  method_name do |param|
    # block code here
  end

Flow:

  ┌─────────────┐
  │ method call │
  └─────┬───────┘
        │
        ▼
  ┌─────────────┐
  │  method body│
  │  executes   │
  │  block code │
  └─────────────┘
Build-Up - 7 Steps
1
FoundationWhat is a Ruby block?
🤔
Concept: Introduce the basic idea of a block as a chunk of code passed to a method.
In Ruby, a block is code between do..end or curly braces {} that you can pass to a method. For example: [1, 2, 3].each do |number| puts number * 2 end Here, the block is the code inside do..end, which runs for each element.
Result
The program prints: 2 4 6
Understanding blocks as code chunks passed to methods is the foundation for flexible Ruby programming.
2
FoundationSyntax differences: do..end vs curly braces
🤔
Concept: Explain the two ways to write blocks and their stylistic differences.
Ruby lets you write blocks with do..end or curly braces {}. Both work similarly but have style preferences: - do..end is usually for multi-line blocks. - {} is usually for single-line blocks. Example: [1, 2, 3].each { |n| puts n } is the same as [1, 2, 3].each do |n| puts n end
Result
Both print: 1 2 3
Knowing the two block syntaxes helps you write clearer, more idiomatic Ruby code.
3
IntermediateBlock parameters and yield keyword
🤔Before reading on: do you think a method can run a block without naming it explicitly? Commit to yes or no.
Concept: Introduce how methods receive blocks implicitly and run them with yield.
Methods don't name blocks as parameters but can run them using the yield keyword. Example: def greet puts "Hello" yield if block_given? puts "Goodbye" end greet do puts "Nice to meet you" end Here, yield runs the block passed to greet.
Result
Output: Hello Nice to meet you Goodbye
Understanding yield shows how Ruby methods can run blocks without explicit parameters, making code concise.
4
IntermediateCurly braces vs do..end precedence
🤔Before reading on: which has higher precedence in Ruby, curly braces or do..end? Commit to your answer.
Concept: Explain how curly braces bind tighter than do..end in method calls.
Curly braces {} have higher precedence than do..end. This affects how Ruby parses chained method calls. Example: puts [1, 2, 3].map { |n| n * 2 }.inspect runs map with the block, then calls puts. But puts [1, 2, 3].map do |n| n * 2 end calls puts with the array, then runs map without a block (which returns an Enumerator).
Result
First prints: [2, 4, 6] Second prints: [1, 2, 3]
Knowing block precedence prevents subtle bugs in chaining methods with blocks.
5
IntermediateBlocks as closures capturing variables
🤔Before reading on: do you think blocks can access variables defined outside them? Commit to yes or no.
Concept: Show that blocks remember and use variables from their surrounding context.
Blocks are closures, meaning they capture variables from where they are defined. Example: factor = 3 [1, 2, 3].each do |n| puts n * factor end Here, factor is used inside the block even though it is defined outside.
Result
Output: 3 6 9
Understanding closures explains how blocks can use external data, enabling powerful patterns.
6
AdvancedConverting blocks to Proc objects
🤔Before reading on: can you pass a block as a variable to another method? Commit to yes or no.
Concept: Explain how blocks can be turned into Proc objects for more flexibility.
Blocks are not objects by default, but you can convert them to Proc objects using &. Example: def call_twice(proc) proc.call proc.call end my_proc = Proc.new { puts "Hello" } call_twice(my_proc) You can also pass a block to a method as a Proc parameter: def greet(&block) block.call end greet { puts "Hi" }
Result
Output: Hello Hello Hi
Knowing how to convert blocks to Procs unlocks advanced Ruby features like storing and passing code as objects.
7
ExpertSubtle differences in block return behavior
🤔Before reading on: does return inside a block exit the method containing the block or just the block? Commit to your answer.
Concept: Explore how return behaves differently inside blocks, Procs, and lambdas.
A return inside a block exits the method that yielded to the block. Example: def test [1, 2, 3].each do |n| return n if n == 2 end 99 end puts test This prints 2 because return exits test. But inside a lambda (a special Proc), return exits only the lambda, not the method. Understanding this difference is crucial for control flow.
Result
Output: 2
Knowing how return behaves prevents unexpected exits and bugs in complex block and Proc usage.
Under the Hood
When Ruby encounters a method call with a block, it does not pass the block as a normal argument. Instead, the block is attached to the method call internally. The method can run the block using yield or by converting it to a Proc object. Blocks are closures, so they keep references to variables from their defining scope. The Ruby interpreter manages this by storing the block's code and environment, allowing it to execute later within the method's context.
Why designed this way?
Ruby was designed to be expressive and flexible. Blocks provide a lightweight way to pass behavior without the overhead of creating objects explicitly. The do..end and {} syntaxes offer readability and style options. The implicit block passing with yield keeps method signatures clean. Alternatives like explicit function objects were more verbose and less elegant, so blocks became a core Ruby feature.
Method call with block:

┌───────────────┐
│  Method call  │
│  with block   │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Ruby runtime   │
│ attaches block│
│ to call frame  │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Method body   │
│ executes code │
│ calls yield() │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Block code    │
│ runs with     │
│ captured vars │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does a block always have to be passed explicitly as a named parameter? Commit to yes or no.
Common Belief:Blocks must be passed as named parameters to methods to be used.
Tap to reveal reality
Reality:Blocks are passed implicitly and accessed inside methods using yield or &block, without naming them as regular parameters.
Why it matters:Thinking blocks need explicit parameters leads to confusion and incorrect method definitions.
Quick: Do do..end and curly braces {} blocks behave exactly the same in all situations? Commit to yes or no.
Common Belief:do..end and {} blocks are interchangeable with no difference.
Tap to reveal reality
Reality:They differ in precedence and style: {} binds tighter and is preferred for single-line blocks, do..end for multi-line.
Why it matters:Ignoring this causes subtle bugs in method chaining and reduces code clarity.
Quick: Does return inside any block always exit only the block? Commit to yes or no.
Common Belief:Return inside a block only exits the block, not the surrounding method.
Tap to reveal reality
Reality:Return inside a block exits the entire method that yielded to the block, which can cause unexpected behavior.
Why it matters:Misunderstanding this leads to bugs where methods exit early unexpectedly.
Quick: Can blocks be stored and passed around like normal objects without conversion? Commit to yes or no.
Common Belief:Blocks are objects and can be assigned to variables directly.
Tap to reveal reality
Reality:Blocks are not objects by default; they must be converted to Proc objects to be stored or passed as variables.
Why it matters:Assuming blocks are objects causes errors when trying to reuse or pass them.
Expert Zone
1
Blocks can capture and modify variables from their defining scope, which can lead to unexpected side effects if not carefully managed.
2
The difference in precedence between do..end and {} affects not only readability but also how Ruby parses chained method calls, which can cause subtle bugs.
3
Using &block to convert a block to a Proc creates an object, which has a small performance cost compared to using yield directly.
When NOT to use
Blocks are not suitable when you need to store code for later use or pass it around multiple times; in such cases, use Proc or lambda objects explicitly. Also, avoid blocks when you need strict control over return behavior, as return inside blocks can exit the surrounding method unexpectedly.
Production Patterns
In production Ruby code, blocks are heavily used for iteration (e.g., each, map), resource management (e.g., File.open with a block to auto-close), and callbacks (e.g., Rails controllers). Experts use blocks with care around return behavior and prefer lambdas when precise control is needed.
Connections
Closures in JavaScript
Blocks in Ruby and closures in JavaScript both capture variables from their surrounding scope.
Understanding Ruby blocks as closures helps grasp similar concepts in JavaScript functions, enabling cross-language learning.
Higher-order functions in functional programming
Blocks allow methods to accept code as arguments, similar to higher-order functions that take functions as parameters.
Recognizing blocks as a form of higher-order functions connects Ruby to functional programming principles.
Callbacks in event-driven programming
Blocks serve as callbacks that run in response to events or conditions inside methods.
Knowing blocks as callbacks helps understand asynchronous and event-driven patterns in other programming domains.
Common Pitfalls
#1Using do..end block with chained method calls expecting {} precedence.
Wrong approach:puts [1, 2, 3].map do |n| n * 2 end
Correct approach:puts [1, 2, 3].map { |n| n * 2 }
Root cause:Misunderstanding that do..end has lower precedence than {} causes the block to bind incorrectly, leading to unexpected output.
#2Trying to assign a block directly to a variable.
Wrong approach:my_block = { puts "Hello" }
Correct approach:my_block = Proc.new { puts "Hello" }
Root cause:Blocks are not objects and cannot be assigned directly; they must be converted to Proc objects.
#3Using return inside a block expecting it to exit only the block.
Wrong approach:def example [1, 2, 3].each do |n| return n if n == 2 end 99 end
Correct approach:def example [1, 2, 3].each do |n| next if n != 2 return n end 99 end
Root cause:Return inside a block exits the whole method, which can be surprising if the programmer expects it to exit only the block.
Key Takeaways
Ruby blocks are chunks of code passed to methods using do..end or curly braces {} syntax.
Blocks are executed inside methods using yield or by converting them to Proc objects with &block.
Curly braces have higher precedence than do..end, affecting how blocks bind in method calls.
Blocks are closures that capture variables from their surrounding scope, enabling powerful patterns.
Return inside a block exits the entire method, not just the block, which can cause subtle bugs.