Ruby's first 3.1 preview locates the failing expression
Ruby 3.1.0-preview1 includes error_highlight by default. Its expression-level diagnostics depend on syntax tree information, with specific consequences for exception messages and supported source code.
A line number cannot distinguish two calls to the same method on one line. Yusuke Endoh's proposal for finer error locations starts with data["data"].first["field"]. An undefined [] method on nil could implicate either subscript operation. Knowing the receiver's class does not identify which intermediate value failed.
Ruby 3.1.0-preview1, released on November 9, includes error_highlight to add that missing location. The announcement demonstrates an underline beneath .time in 1.time {}. The gem loads by default, and --disable-error_highlight disables it. This is a preview release, so the useful subject is the implementation available now, including the cases it deliberately leaves unsupported.
I came to Ruby through Perl, and find its object model and readable expressions more approachable for larger applications. Expression-level error reporting fits that preference. Readable source still needs a way to identify the operation that failed when several operations occupy the same line.
Locating an operation requires syntax information
The proposal developed into an argument about the interface used to recover source positions. Benoit Daloze objected to depending on RubyVM::AbstractSyntaxTree, a CRuby-specific API, and preferred location information on the shared backtrace interface. Endoh argued that choosing a useful underline also required knowledge of the syntax tree. Coordinates alone did not describe the receiver and the method call separately.
The accepted approach used an MRI-specific gem and extended the syntax tree API to accept a backtrace location. That choice explains why a seemingly small presentation change involves interpreter internals. The implementation needs to connect an executing operation with the corresponding source expression. It also explains the separation from did_you_mean, which already had users outside this interpreter-specific mechanism.
In the preview's exception extension, the work happens inside to_s. It first duplicates the existing message, takes the first backtrace location, and calls RubyVM::AbstractSyntaxTree.of with keep_script_lines: true. The extension then asks ErrorHighlight.spot to choose a source region and passes that result to a formatter.
This makes the additional text part of the exception's string representation. A consumer reading that representation can encounter a multiline message without asking for a separate diagnostic object. That matters for software comparing error messages exactly. An assertion about the exception class expresses a different requirement from an assertion about every character of its formatted explanation.
Parsing happens when the message needs it
The preview's syntax tree implementation shows how source recovery works. ast_s_of obtains a path, available script lines, and a node identifier. It parses retained source lines when it has them. Otherwise it handles command-line source or parses the source file, then searches the resulting tree for the matching node identifier.
The diagnostic therefore depends on more than the original line number. It needs source text that can produce the corresponding tree. For file-backed source without retained lines, the implementation opens the path and invokes the parser. This is work performed to reconstruct the diagnostic, so the code does not justify treating the underline as metadata that every backtrace already contains in a finished form.
Keeping deployed source consistent with executed code matters to this design. Reconstructing a tree from different file contents cannot establish what the original expression looked like. That is an operational implication of the source-recovery mechanism, rather than a claim that this preview records a complete historical copy of every executed file.
The underline describes the failed operation
The spotter in the preview tag dispatches on syntax tree node types. Ordinary method calls and subscript calls need different treatment. For an ordinary call it finds the method name after the receiver. For [], it locates the brackets and their argument region. Operator calls have their own handling.
Compound assignment makes the distinction especially useful. The expression v[0] += 42 can fail while reading the element, applying +, or writing the result. The spotter inspects the exception's method name to select the relevant region for those operations. Underlining the entire line would lose the distinction that the extra syntax information makes possible.
Its result contains starting and ending line numbers, starting and ending columns, and the source snippet. Those fields separate choosing the region from drawing it. The spotter can also return no result. In this preview it declines snippets containing non-ASCII characters, with a comment identifying multibyte support as unfinished work. That restriction applies to highlighting, not to Ruby's ability to execute such source.
Supported errors have explicit boundaries
The exception extension prepends its module to NameError. It contains branches intended for TypeError and ArgumentError, but the lines that would install the extension on those classes are commented out. The source says test failures caused that temporary restriction. Reading those branches alone would therefore give the wrong account of what the preview enables.
The preview's own diagnostic tests demonstrate the active behavior for missing methods and constants. One case expects the following source fragment and marker inside a NoMethodError message:
nil.foo + 1
^^^^
The missing call is .foo. The marker does not include the following addition, even though both occur on the same source line. Other tests cover subscript calls and each stage of compound assignment. These examples specify which operation the diagnostic identifies, without requiring a claim about every Ruby expression.
The tests also document cases without extra highlighting. A subscript spread across several lines retains its ordinary error message in one such case. A multibyte method name likewise receives no source marker. The argument-error examples appear inside a disabled block, consistent with the installation code. The distinction between implemented helpers and enabled behavior is visible in both places.
I value this kind of precision more than a general promise of better errors. The marker answers which operation failed. It does not explain why the program supplied the wrong value or decide what the application should do with it.
Formatting is a separate policy
The default formatter accepts the spotter's result through message_for. For a region contained on one line, it appends the snippet and a caret marker. It constructs indentation by replacing preceding characters with spaces while preserving tabs, then repeats the caret for the selected column width. A region spanning multiple lines produces an empty addition.
The same file provides ErrorHighlight.formatter= for replacing that presentation policy. Formatter selection uses storage on the current Ractor, with the default formatter as the fallback. A custom formatter receives the location fields and snippet, so it can change the representation without reimplementing the spotter's syntax analysis.
That separation is useful for tools consuming exceptions. A source region and a terminal underline are different representations of the same diagnostic information. This preview supplies both the region-selection mechanism and a default text representation, while leaving a defined method through which another formatter can consume the result.