Ruby Blocks, Enumerators, and Resource Lifetimes
Blocks are the heart of idiomatic Ruby. each, map, select, File.open, Mutex#synchronize, transactions in Rails — all take a block, and many of Ruby's best properties follow from that: concise iteration, composable data pipelines, and resource handling that cleans up automatically, because the method that opens a resource also controls when it is closed.
Enumerators extend this model. Any iteration can become an object that can be chained, made lazy so it processes only what is needed, or iterated externally one element at a time. Used together, blocks and enumerators let Ruby code stream gigabytes of data with constant memory and guarantee that files, sockets, and locks are released — as long as you understand when cleanup runs and the few patterns that defeat it.
TL;DR
- Methods receive a block implicitly (
yield,block_given?) or explicitly (&block); blocks are closures. - Lambdas check arity and
returnfrom themselves; procs are lenient andreturnfrom the enclosing method. - Return an enumerator when no block is given (
enum_for(__method__)), so callers can chain and compose. - Lazy enumerators (
.lazy) process elements on demand and stop early withfirst,take, orfind. - Prefer block-form resource APIs (
File.open { },Tempfile.create { },pool.with { }) andensurefor cleanup. - Internal iteration runs
ensurewhen stopping early; abandoned external enumerators (next) may leave resources open.
Quick Example
Streaming a large CSV file with constant memory, stopping after ten matches, and closing the file in every case:
Rows are read one at a time, first(10) breaks out of the iteration as soon as ten orders match, and the File.open block closes the file whether the loop finishes, stops early, or raises.
Core Concepts
Blocks, Procs, and Lambdas
Use yield for simple cases and &block when you need to store or pass the block along. next exits a block early with a value; break exits the method that yielded. Symbol#to_proc (map(&:name)) and method references (method(:puts)) convert to procs.
Internal and External Iteration
- Internal iteration (
each { |x| ... }): the collection drives the loop and calls your block — simple and efficient, andensureclauses run when the loop ends or breaks. - External iteration (
enum.next,enum.peek): you pull elements one by one; Ruby uses a Fiber behind the scenes, andStopIterationsignals the end. Useful for merging streams or interleaving sources.
Building Enumerators
enum_for / to_enum turn any block-taking method into an enumerator. Enumerators chain with with_index, each_slice, each_with_object, and zip; Enumerator.produce generates sequences from a seed.
Lazy Enumerators
Normal map and select build full intermediate arrays. .lazy composes the pipeline and pulls elements through it one at a time, so infinite or huge sources work and early-exit methods (first, take(n).to_a, find) stop reading. Use eager or to_a to materialize. Lazy pipelines have per-element overhead, so they help for large or early-terminating streams, not for small arrays.
Resource Lifetimes
For your own resources, use begin ... ensure ... end and offer a block-form API that guarantees cleanup. See Error Handling.
Best Practices
Return Enumerators From Iteration Methods
Use return enum_for(__method__, ...) unless block_given? so callers can chain, lazify, and test easily.
Use Block-Form Resource APIs
Let the method that acquires a resource release it; avoid manual close calls in application code.
Stream Large Data Lazily
Read files, query results, and API pages incrementally with enumerators and each_slice batches. In Rails, use find_each or in_batches. See Ruby on Rails.
Prefer Lambdas for Stored Callables
Lambdas' strict arity and local return make stored callbacks predictable.
Common Mistakes
Opening Without a Block
Abandoned External Enumerators
return Inside a Proc
FAQ
What is the difference between a proc and a lambda in Ruby?
Lambdas enforce the number of arguments and treat return as returning from the lambda. Procs ignore missing or extra arguments and return from the method where the proc was defined.
What does enum_for(__method__) do?
It returns an Enumerator that, when iterated, calls the current method with a block — letting an iteration method be used both with a block and as a chainable enumerator.
When should I use lazy enumerators?
For large, streaming, or infinite sources and for pipelines that stop early, such as finding the first N matches in a big file. For small collections, eager methods are simpler and faster.
Does breaking out of a block close resources?
Yes, for internal iteration: break, return, and exceptions unwind through the method's ensure clauses, so block-form APIs like File.open close their resources.
Why use File.open with a block?
The block form guarantees the file is closed when the block exits, including when an exception is raised, without explicit close or ensure code.
Related Topics
- Ruby — The Ruby language
- Ruby on Rails — Batches, transactions, and blocks in Rails
- Python Generators & Iterators — A comparable lazy iteration model
- Error Handling Patterns — Cleanup and exceptions
- Memory Management — Streaming instead of loading everything
- Node.js Streams and Backpressure — Streaming data in another runtime