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

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

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

References