Preparing for Ruby 3.4: New Features and Syntax Changes to Expect
In 1845, when the Great Western Railway in the UK needed to convert its broad-gauge tracks to standard gauge, it faced a monumental task: replacing the underlying infrastructure without completely halting the transportation network. The transition required meticulous planning and temporary dual-gauge tracks to maintain compatibility.
Similarly, when a programming language changes its fundamental parsing infrastructure, the transition must be handled with care. With the release of Ruby 3.4, we see significant changes to the language’s underlying tracks — most notably the introduction of the Prism parser — along with performance optimizations and memory management improvements. This release introduces new syntax and lays the groundwork for long-term maintainability and speed.
In this guide, we will explore the key changes in Ruby 3.4, including the new Prism parser, YJIT improvements, and the transition path for frozen string literals. Understanding these updates will help you prioritize technical debt remediation and ensure a smoother upgrade process.
The Shift to the Prism Parser
A substantial internal change in Ruby 3.4 is the adoption of Prism as the default parser. While this is primarily an under-the-hood improvement, it is a critical step forward for the Ruby ecosystem.
For years, CRuby relied on parse.y — a Bison grammar file — to understand Ruby code. This imperative, C-centric approach meant that the parser was deeply intertwined with the Ruby interpreter itself. If a tool like RuboCop or a language server needed to parse Ruby code, it often had to rely on a completely separate, third-party parser, leading to subtle inconsistencies.
Prism, on the other hand, is a standalone, portable recursive descent parser. It is designed to be error-tolerant and to integrate cleanly into other tools. This means that, moving forward, the entire Ruby ecosystem can rely on a single, unified understanding of Ruby syntax.
Of course, changing the parser is a significant shift. If you encounter compatibility issues during the upgrade, you can temporarily revert to the conventional parser by passing the --parser=parse.y flag. We recommend, though, addressing any parsing anomalies head-on, as Prism represents the foundation of the language going forward.
Syntax Refinements: The it Block Parameter
Ruby 3.4 introduces a new default block parameter: it. This provides a clearer alternative to the _1 numbered parameter when working with single-parameter blocks.
Strictly speaking, numbered parameters like _1 and _2 (introduced in Ruby 2.7) solved the problem of naming block variables, but they can occasionally read like mathematical formulas rather than expressive code. The it keyword is designed to be used in straightforward, one-line blocks where the context speaks for itself.
For example, if you had an array of names you wanted to capitalize, let’s look at this progression in an irb session. First, the traditional approach:
$ irb
irb(main):001> users = ["alice", "bob", "charlie"]
=> ["alice", "bob", "charlie"]
irb(main):002> users.map { |user| user.capitalize }
=> ["Alice", "Bob", "Charlie"]
We can also use the numbered parameter introduced in Ruby 2.7:
irb(main):003> users.map { _1.capitalize }
=> ["Alice", "Bob", "Charlie"]
And now, the new Ruby 3.4 approach:
irb(main):004> users.map { it.capitalize }
=> ["Alice", "Bob", "Charlie"]
You might wonder: if we have _1, why do we need it? The answer is straightforward: readability. We recommend using it strictly for straightforward cases. If your block logic requires multiple lines or complex transformations, explicit block variables remain the most maintainable choice.
YJIT Enhancements and Core Methods in Ruby
The YJIT compiler continues to mature in Ruby 3.4. A notable improvement is the reduction in memory usage through compressed metadata, alongside the introduction of a unified memory limit via the --yjit-mem-size option.
One may wonder: how can Ruby code execute faster than C code? The answer lies in how YJIT operates. Historically, core methods like Array#map were written in C for speed. Now, the core team is beginning to rewrite several of these primitives directly in Ruby. Because YJIT is capable of aggressively inlining Ruby code at runtime, these native Ruby implementations can actually execute faster than their traditional C counterparts — eliminating the overhead of switching between Ruby and C contexts.
To take full advantage of these optimizations, you should ensure your application is running with YJIT enabled in production environments, typically by passing the --yjit flag or setting the RUBY_YJIT_ENABLE=1 environment variable.
The Path to Default Frozen String Literals
Ruby has been slowly moving toward immutable string literals by default. In Ruby 3.4, mutating a string literal in a file that lacks a # frozen_string_literal: true comment will emit a deprecation warning.
This change improves memory utilization and reduces garbage collection overhead, as identical string literals can share the same object in memory.
Let’s look at a concrete example. If you run the following script in Ruby 3.4 with deprecation warnings enabled:
# test_string.rb
my_string = "hello"
my_string << " world"
puts my_string
$ ruby -W:deprecated test_string.rb
test_string.rb:3: warning: literal string will be frozen in the future
hello world
Additionally, note that the exact warning text or line numbers you see in your particular environment might vary slightly.
To prepare your codebase, you should run your test suite with these warnings enabled. When you identify a warning, you have a few options. If you truly need a mutable string, you can use String.new("hello") or "hello".dup. Alternatively, you can add the magic comment # frozen_string_literal: true to the top of your files to opt-in to the future behavior today.
Modular Garbage Collection
Large-scale applications frequently encounter bottlenecks related to garbage collection. Ruby 3.4 introduces a Modular GC API, splitting the built-in garbage collector into a separate, pluggable component.
This architecture allows developers to compile Ruby with alternative garbage collectors, such as the experimental MMTk (Memory Management ToolKit) implementation. Of course, this is currently an experimental feature, and most applications will continue to use the default garbage collector. However, it demonstrates that the core team is actively exploring ways to handle large numbers of object allocations more efficiently. For organizations dealing with heavy memory pressure, this modular approach typically opens the door to specialized, high-performance GC tuning in the future.
Networking Improvements: Happy Eyeballs Version 2
For applications making outbound network requests, network timeouts and slow connections can be a significant source of latency. To address this, Ruby 3.4 implements Happy Eyeballs Version 2 (RFC 8305) in the socket library.
Before we get into that, though, let’s establish what problem this solves. Historically, when connecting to a server that supports both IPv4 and IPv6, a client might try to connect via IPv6 first. If the IPv6 network was misconfigured, the client would wait for a long timeout before falling back to IPv4.
With this update, TCPSocket and Socket.tcp will perform IPv6 and IPv4 name resolution concurrently. The engine will then attempt connections to the resolved addresses with parallel attempts staggered at 250ms intervals, returning the first successful connection. This translates to minimized connection delays and a more resilient application when dealing with routing issues.
Preparing for the Upgrade
A practical approach to preparing for Ruby 3.4 is to run your CI pipeline against the new release as soon as possible. We recommend focusing on a few key areas:
- Resolving deprecation warnings, particularly those related to string mutation, by enabling
-W:deprecated. - Ensuring your test suite passes with the new Prism parser, falling back to
--parser=parse.yonly if absolutely necessary for debugging. - Monitoring memory and CPU utilization with YJIT enabled to establish a new performance baseline.
Upgrading a language’s underlying infrastructure is not unlike replacing a railway’s tracks. By systematically addressing these changes and testing early, you ensure that your application’s transition to Ruby 3.4 remains smooth, fast, and maintainable for years to come.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming