RailsAgency
← All articles

Ruby

What Ruby 3.4 Changes for a Rails App Still on 3.3

By Daniel Rice · · 3 min read

Ruby 3.4 is a small upgrade from 3.3, and that is the point. A Rails app does not need a rewrite to take it. The release adds a clearer block parameter, switches the default parser to Prism, puts a single cap on YJIT memory, and makes the garbage collector replaceable. None of those change how you write a controller. Two of them change what you look at after the deploy.

Ruby 3.4.0 shipped on December 25, 2024. The notes are on ruby-lang.org.

it instead of _1

Numbered block parameters have been around since 3.0. _1 is fine until a block grows a second use and someone wonders whether _2 is allowed. Ruby 3.4 adds it for the case where there is one parameter and the name would not have told you anything.

ary = ["foo", "bar", "baz"]
ary.map { it.upcase }
# => ["FOO", "BAR", "BAZ"]

it behaves like _1. Use it in a one-line block where the receiver is obvious: users.select { it.admin? }, names.map { it.strip }. When the block does three things, name the parameter. An agent reading a diff has the same problem a person does. it says "this is the only argument." _1 says "there might be a _2 somewhere."

Prism is the parser, unless you opt out

The default parser is now Prism. The old parse.y parser is still there. If a gem or a syntax error only shows up on 3.4, the first check is to run the same file with the previous parser:

ruby --parser=parse.y -c app/models/invoice.rb

For almost every Rails app this is an internal swap. You should not see different method results because the parser changed. You might see a different error message, and that message is the thing to paste into the upgrade notes. If Prism and parse.y disagree, that disagreement is a bug report to Ruby, not a reason to stay on 3.3.

YJIT gets one memory number

YJIT in 3.4 is faster on the usual benchmarks and uses less memory for its own metadata. The operational change is the limit. --yjit-mem-size tracks total YJIT memory and defaults to 128MiB. It replaces the older --yjit-exec-mem-size as the knob you actually want.

On a Puma or Sidekiq host, turn YJIT on the way you already do, then watch that cap. If a worker is compiled into a corner, RubyVM::YJIT.log is the tail of the compilation log at runtime, and --yjit-log records it from the start. RubyVM::YJIT.runtime_stats now always includes invalidation, inlining, and metadata numbers, so you do not need a special stats build to see whether the compiler is giving up.

The code generation changes are why a 3.4 process can spend less time in the interpreter without a Rails patch. When YJIT is on, Array#each, Array#select, and Array#map are written in Ruby so the compiler can see them. It can also inline a small method that is empty, returns a constant, returns self, or returns an argument directly. You do not call a new API for that.

The collector can be swapped, if you build for it

Ruby 3.4 can load an alternative garbage collector through a modular GC. You opt in when Ruby is compiled, with --with-modular-gc, and pick the library at runtime with RUBY_GC_LIBRARY. A normal install still uses the built-in collector. This is not a setting in the Rails app, and the upgrade is not a GC tuning project.

What we check on the way up

The app boots on 3.4 in CI before it boots in production. The suite is the compatibility test. After that, the production check is YJIT memory and the error log for the first day, not a line-by-line reread of the app.

A few call sites are worth a deliberate pass. One-line blocks that use _1 can become it when you next touch them. Do not send an agent through the whole repo to rename them. A block you are already editing is the right moment.

When a Rails app is still pinned to 3.3 because nobody wants to own the bump, this is the size of the bump. Rails Agency will take the app to 3.4, confirm the suite and the YJIT cap, and leave the rest of the syntax where it is.

FAQ

Will Prism change how my Rails app behaves?
It should not change method results. If a file only fails on 3.4, rerun it with ruby --parser=parse.y -c and treat a disagreement as a parser bug, not an app rewrite.

Dealing with this in your own Rails app?

Book a free 30-minute call. You'll talk directly to a senior Rails engineer — no sales team, no hand-offs.