December 25, 2017·6 min read·engineeringrubyblockstestingruntime

Ruby 2.5 makes block results and failure paths more explicit

Ruby 2.5 adds yield_self and exception clauses directly inside do/end blocks. Branch and method coverage provide more precise evidence about executed paths, while runtime work reduces unused tracing overhead.

A block is already one of Ruby's main tools for expressing iteration and scoped work. Ruby 2.5 gives it two additional roles with less surrounding syntax. A do/end block can contain its own exception clauses, and yield_self can pass a value into a transformation while returning the transformation's result. The release also supplies branch and method coverage to distinguish execution paths that line coverage can collapse together.

The December 25 announcement identifies 2.5.0 as the first stable release in the series. Its language additions are small enough to understand individually, but they affect how a reader follows results and failures through a program. A block can state its local recovery behavior, and a chain can state whether the receiver or the block's result continues to the next operation.

I find Ruby's blocks and collection methods an elegant way to express common operations. These additions are useful when they make that flow easier to read. Shorter syntax alone is insufficient if a reader can no longer tell which value is returned or which failures a recovery clause handles.

Exception clauses can belong directly to a block

The 2.5.0 NEWS file records support for rescue, else and ensure inside do/end blocks under feature 12906. A block body no longer needs an extra begin/end solely to attach those clauses. The change is specifically described for do/end, so it should not be generalized to every block spelling.

This example converts independent input strings, treating an invalid numeric value as an absent result for that item. The exception class is explicit, and the recovery is attached to the block performing the conversion.

inputs = ["12", "bad", "7"]
numbers = inputs.map do |text|
  Integer(text)
rescue ArgumentError
  nil
end

The block's placement determines the intended scope. Conversion happens separately for each element, and the block returns either the converted integer or nil. Moving recovery around the whole iteration would describe a different control flow. Removing a begin does not remove the need to choose that scope deliberately.

The same syntax supports an ensure clause for cleanup associated with the block body and an else clause for the non-exception path. Those clauses let the source keep related behavior together. They do not require a new exception type or a different iteration API.

This matters when a method already takes a block to describe scoped work. An extra nested exception construct can obscure the relationship between the work and its recovery. Ruby 2.5 permits the block itself to carry that relationship while preserving explicit exception classes and clauses for the reader.

yield_self continues with the block's result

The tagged implementation and documentation define yield_self as yielding the receiver to a block and returning the block's result. It is exposed through Kernel. The release announcement contrasts it with tap, which returns the original receiver.

value = 4.yield_self { |number| number * number }
# value == 16

original = 4.tap { |number| number * number }
# original == 4

The arithmetic inside the blocks is identical, but the surrounding expression has a different result. That distinction determines what a following method in a chain receives. A transformation should use the operation whose return contract agrees with the intended next step.

yield_self can be useful when the next operation is naturally a function of the current value rather than a method on it. The block names that input and can return a different type. The source documents a chain that computes a value inside the block and then calls to_s on the result, demonstrating that the transformed value continues through the expression.

The implementation yields the receiver as a block argument. It does not change the block's receiver through an instance-evaluation operation. That is an important reading of the release's concise description of yielding in context. Inside the block, the explicit parameter is the original object, and ordinary block rules still determine other names.

I value having a method name that distinguishes transformation from inspection or mutation of the same receiver. A long chain can still be difficult to read, but the result contract is precise. Using tap where the block's new value is intended would silently continue with the wrong object.

Coverage can record alternatives within executed code

Ruby 2.5 adds branch and method coverage. Branch coverage identifies which alternatives executed, while method coverage identifies invoked methods. A test suite can execute a line containing a conditional without exercising every outcome of that conditional, so line execution alone cannot answer the branch question.

The coverage extension accepts explicit measurement targets. Passing :all selects lines, branches and methods. A hash can choose the targets individually. Calling Coverage.start without arguments retains the compatible line-coverage mode.

require "coverage"
Coverage.start(lines: true, branches: true, methods: true)
require_relative "subject"
results = Coverage.result

Here subject.rb is the code being measured. Coverage starts before that file is loaded, following the extension's documented usage. The result is collected after the required code has run. An application test harness would place the relevant test execution before collecting the result as well.

The implementation rejects a target hash that selects nothing and prevents changing measurement targets during an active measurement. Coverage.result obtains the recorded result and resets coverage, ending the measurement. These lifecycle rules matter to a tool integrating coverage into an existing test process.

The extension also labels the feature experimental. A coverage tool can use the new measurements while recognizing that this API has that status. Reporting branch coverage should not be confused with proving correctness. It establishes that alternatives executed, while assertions determine whether the resulting behavior matched expectations.

Method coverage supplies another view of the same suite. A method can exist in loaded source yet never be invoked by the tests. Recording that absence helps distinguish available code from exercised code. Combining it with branch data provides more specific evidence than a single aggregate line percentage.

Thread failures become visible by default

The release changes Thread.report_on_exception to true by default. The NEWS file describes unhandled exceptions terminating threads being shown on standard error. This makes a failure in another thread more visible even when the application does not immediately join it.

The change is reporting behavior. It should not be described as automatically recovering the thread or making its work succeed. An application still needs to decide how a failed background operation affects its own result. The new default gives that decision more visible evidence when an exception goes unhandled.

Backtrace presentation also changes experimentally when printed directly to a terminal, placing the most recent call last. The announcement limits the reversal to direct terminal output. Consumers parsing another representation should not assume every backtrace has globally changed order.

These changes complement the block-level syntax without being the same mechanism. A local rescue clause defines recovery in code. A thread exception report exposes a failure that escaped handling. Both affect how an application developer sees failure, but they operate at different stages.

Unused tracing instructions leave the ordinary execution path

The performance work includes removing trace instructions from normal instruction sequences and using dynamic instrumentation for TracePoint. The announcement reports roughly five to ten percent improvement from eliminating those instructions, which ordinarily impose overhead when TracePoint is unused. This is a maintainer-reported result for that change, not a speed guarantee for every application.

Lazy Proc allocation also improves forwarding a block parameter, with the announcement reporting about a threefold improvement for that operation compared with Ruby 2.4. The specific example is a method forwarding &b to another method. It does not imply that every block invocation or complete program becomes three times faster.

The collection API also gains more specific transformations. Hash#slice selects named entries, while Hash#transform_keys and its mutating counterpart provide a direct operation on keys. These complement the value transformations introduced earlier without requiring each caller to rebuild a hash through general iteration. A key transformation still needs to agree with the application's intended key space.

Struct classes can additionally request keyword initialization through keyword_init: true. Naming the members at construction can make a call easier to read when several values have similar types. This is an opt-in class-definition setting, so the release does not require every existing Struct constructor to change its calling convention.

These runtime changes reduce costs beneath existing source while the language additions provide new ways to state control flow. A program can adopt Ruby 2.5 without immediately rewriting every block. Where the new syntax is useful, its result and exception boundaries remain explicit, and the new coverage targets can record which branches and methods the tests actually execute.