What's new in Ruby 4.0
Ruby 4.0 will be released next week on Christmas day. This release brings a new JIT compiler, improvements to Ractors, a new mechanism to define namespaces called Ruby::Box, and a whole lot of other changes.
Although it’s a major version bump, there shouldn’t be any serious breaking changes. This version bump is to celebrate 30 years since the first public release of Ruby.
Ruby::Box
Ruby::Box is an experimental feature that brings isolated namespaces to Ruby. This can be enabled by setting the RUBY_BOX=1 environment variable. This can allow you to do things like loading two versions of a library at the same time like this:
# foo_v1.rb
class Foo
def hello
"Foo version 1"
end
end
# foo_v2.rb
class Foo
def hello
"Foo version 2"
end
end
# main.rb
v1 = Ruby::Box.new
v1.require("./foo_v1")
v2 = Ruby::Box.new
v2.require("./foo_v2")
v1::Foo.new.hello #=> "Foo version 1"
v2::Foo.new.hello #=> "Foo version 2"
I find the syntax rather confusing, with the need for instantiating a Box object. But this is still an experimental feature, so we’ll hopefully have better ergonomics with the final version.
Ractor
Ractor’s API has been redesigned to use Ractor::Port as the means for communicating between ractors. As a result Ractor.yield and Ractor#take have been removed. Now, you would use a ractor port like this:
port = Ractor::Port.new
Ractor.new(port) do |p|
p << "first value"
p << "second value"
end
puts port.receive #=> "first value"
puts port.receive #=> "second value"
In Ruby 3.4, this would have looked like this:
ractor = Ractor.new do
Ractor.yield "first value"
Ractor.yield "second value"
end
puts ractor.take # => "first value"
puts ractor.take # => "second value"
ZJIT
A new JIT compiler called ZJIT has been merged into Ruby. This implements a method based JIT compiler, compared to the lazy basic block versioning compiler that YJIT uses. Using a more traditional type of JIT will hopefully make the codebase more accessible to new contributors.
Although ZJIT is faster than the interpreted code, it hasn’t yet caught up with YJIT. The latter is still the recommended JIT for production. However, this sets the stage for some more speedups in the next year.
Logical operators on the next line
The following syntax is now allowed for logical operators and, or, && and ||.
if condition1?
&& condition2?
&& condition3?
# do something
end
# The above is the same as what we can currently do with:
if condition1? &&
condition2? &&
condition3?
# do something
end
Ruby top level module
The Ruby top level module was reserved in Ruby 3.4, but now it actually has some constants defined in it:
Ruby::VERSION
#=> "4.0.0"
Ruby::DESCRIPTION
#=> "ruby 4.0.0preview2 (2025-11-17 master 4fa6e9938c) +PRISM [x86_64-darwin24]"
# Other constants in the module:
Ruby.constants
#=> [:REVISION, :COPYRIGHT, :ENGINE, :ENGINE_VERSION, :DESCRIPTION,
# :VERSION, :RELEASE_DATE, :Box, :PLATFORM, :PATCHLEVEL]
instance_variables_to_inspect
When inspect is called on an object, it includes all instance variables, including memoization variables, which can get noisy in larger classes. For instance, the @area variable shows up below after the area method is called.
class Square
def initialize(width)
@width = width
end
def area
@area ||= @width * @wdith
end
end
square = Square.new(5)
puts square #=> #<Square:0x000000011f280ff8 @width=5>
sqare.area #=> 25
puts square #=> => #<Square:0x000000011f280ff8 @area=25 @width=5>
However, by defining which variables should be shown like this, we can make the inspect output less noisy.
class Square
# ...
private
def instance_variables_to_inspect = [:@width]
end
square.area # 25
puts square #=> => #<Square:0x000000011f280ff8 @width=5>
Array#rfind
Array#rfind has been implemented to find the last element matching a condition. This is a more efficient alternative to reverse_each.find. It avoids an array allocation that happens when calling Enumerable#reverse_each.
# new
[2, 2, 3, 4, 6, 7, 8].rfind(&:odd?) #=> 7
# old
[2, 2, 3, 4, 6, 7, 8].reverse_each.find(&:odd?) #=> 7
At the same time, Array#find has also been added, which is a more efficient implementation than Enumerable#find that was being used before.
New core classes
Useful classes like Set and Pathname were previously not autoloaded. This meant that you had to do require "set" or require "pathname" before you could use them. This is no longer the case, and you can use these classes without the require.
Other changes
- Object allocations are significantly faster - over 2x without JIT and almost 4x with JIT enabled.
- RJIT has been extracted into a separate gem.
- CGI library has been removed from default gems, but a few commonly used features such as
CGI.escapeand related methods are retained and can be used by requiringcgi/escape.
Further reading
This post highlights changes that I personally found most interesting, and skip over features I might not use. If you’re looking for a more comprehensive look at the release, I highly recommend looking at the release announcement, and changelog.