It is that time of the year again! As tradition dictates, Christmas is approaching, and with it comes the gift of a new Ruby release.
I experimented with Ruby 4.0.0-preview3 for a few days, and I am excited to share with you the highlights of this upcoming version.
Let’s dive into what’s new and what makes this release special.
What’s new in core classes
Ruby::Box
This new feature was introduced to provide a way to create isolated context executions within the same Ruby process.
It allows you to run code in a separate "box" that has its own global variables, constants, and class definitions.
Take the following code as an example:
# file: greetings.rb
class String
def shout
self.upcase + "!"
end
end
class Greetings
def self.say_hello(name)
"Hello, #{name}!".shout
end
endNow, let’s put this into its own box:
# file: demo.rb
box = Ruby::Box.new
box.require_relative('greetings')
puts box::Greetings.say_hello "Edy" # Output: HELLO, EDY!
"Hello".shout # This will raise NoMethodError since the String#shout method is not defined in the root box (main context)This feature is still experimental and must be enabled using the RUBY_BOX=1 environment variable.
A use case that comes to mind is running multiple versions of the same library simultaneously. For example, migrating a Service implementation from v1 to v2 while needing to keep both versions running for a while.
Whether it is a gem or your own code, you can load each version in its own box and use them without conflicts. This blog post from Xavier Noria has more details about Ruby::Box.
Ractor
As someone with experience in Erlang, I am happy with the Ractor improvements in this release. It is becoming more ergonomic and easier to use.
Ractor, Ruby’s implementation of the Actor Model, works like this:

This version introduces Ractor::Port, which essentially acts as the mailbox in the Actor Model. Messages are now sent to and received from ports, representing a significant improvement in data synchronization between Ractors.
Any Ractor can send messages to any port, but only the owner Ractor can receive messages from its own port.
As a result of this change, methods like Ractor.yield and Ractor#take are no longer necessary and have been removed.
Two other additions to Ractor are Ractor.shareable_proc and Ractor.shareable_lambda. Sharing procs is necessary and, once again, makes everything more ergonomic.
One of the "Hello World" examples of the Actor Model is the parallel map. Here is how it looks in Ruby 4.0:
def pmap(enum, &block)
raise 'Block must be shareable. (see Ractor.shareable_proc and Ractor.shareable_lambda)' unless Ractor.shareable?(block)
ports = enum.map do |item|
p = Ractor::Port.new
Ractor.new(p,block, item) do |p, block, item|
p << "[#{Time.now}] Monotonic nanosecond: #{Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)} | Result: #{block.call(item)}"
end
p
end
while !ports.empty?
p, result = Ractor.select(*ports)
puts "#{p}: #{result}"
ports.delete p
end
end
# Example usage
b = Ractor.shareable_proc { |x| x * x }
pmap(1..5, &b)
# Output:
# parallel_map.rb:6: warning: Ractor API is experimental and may change in future versions of Ruby.
# #<Ractor::Port:0x000000011d479708>: [2025-12-18 12:31:54 -0300] Monotonic nanosecond: 434548010104000 | Result: 1
# #<Ractor::Port:0x000000011d479050>: [2025-12-18 12:31:54 -0300] Monotonic nanosecond: 434548009883000 | Result: 4
# #<Ractor::Port:0x000000011d478df8>: [2025-12-18 12:31:54 -0300] Monotonic nanosecond: 434548010104000 | Result: 9
# #<Ractor::Port:0x000000011d478bf0>: [2025-12-18 12:31:54 -0300] Monotonic nanosecond: 434548010437000 | Result: 16
# #<Ractor::Port:0x000000011d4789e8>: [2025-12-18 12:31:54 -0300] Monotonic nanosecond: 434548010449000 | Result: 25We spawn a Ractor for each item in the enumerable, passing a port and a shareable proc as arguments. Each Ractor computes the result and sends it back through its port.
Trying to do this in Ruby 3.x reveals how cumbersome it used to be, requiring message tagging and significant boilerplate code.
It is worth mentioning that Ractor is still experimental and may change in future Ruby versions.
Kernel
The Kernel#inspect method now checks for the existence of #instance_variables_to_inspect, allowing control over which instance variables appear in the #inspect output:
class DatabaseConfig
def initialize(host, user, password)
@host = host
@user = user
@password = password
end
private def instance_variables_to_inspect = [:@host, :@user]
end
conf = DatabaseConfig.new("localhost", "root", "hunter2")
conf.inspect #=> #<DatabaseConfig:0x0000000104def350 @host="localhost", @user="root">ArgumentError now displays code snippets for caller and callee
This improvement eases debugging by providing more context about the error’s location.
# file: greetings.rb
class Greetings
def self.say_hello(name)
"Hello, #{name}!"
end
end
# file: demo.rb
require_relative 'greetings'
Greetings.say_helloThe code above will raise the following error in Ruby 4.0:
~/greetings.rb:2:in 'Greetings.say_hello': wrong number of arguments (given 0, expected 1) (ArgumentError)
caller: demo.rb:3
| puts Greetings.say_hello()
^^^^^^^^^^
callee: ~/greetings.rb:2
| def self.say_hello(name)
^^^^^^^^^^
from demo.rb:3:in '<main>'A much more informative error message that helps identify the problem quickly.
New top-level module: Ruby
Ruby is a new top-level module containing Ruby-related constants.
%w[
Ruby::VERSION
Ruby::RELEASE_DATE
Ruby::PLATFORM
Ruby::PATCHLEVEL
Ruby::REVISION
Ruby::COPYRIGHT
Ruby::ENGINE
Ruby::ENGINE_VERSION
Ruby::DESCRIPTION
].each do |const_name|
puts "#{const_name} = #{Ruby.const_get const_name.split("::").last}"
end
# Output:
# Ruby::VERSION = 4.0.0
# Ruby::RELEASE_DATE = 2025-12-17
# Ruby::PLATFORM = arm64-darwin24
# Ruby::PATCHLEVEL = -1
# Ruby::REVISION = 54d3945ee53fa29186c6aa44f673a3a5ec3b38d9
# Ruby::COPYRIGHT = ruby - Copyright (C) 1993-2025 Yukihiro Matsumoto
# Ruby::ENGINE = ruby
# Ruby::ENGINE_VERSION = 4.0.0
# Ruby::DESCRIPTION = ruby 4.0.0dev (2025-12-17T07:16:32Z v4.0.0-preview3 54d3945ee5) +PRISM [arm64-darwin24]What’s new in the language
*nil no longer calls nil.to_a
Before Ruby 4.0, every time you splat nil, it would call nil.to_a, which returns an empty array.
class NilClass
def to_a
puts "Calling nil.to_a"
[]
end
end
[*nil]
# Output: Calling nil.to_aRunning the same code in Ruby 4.0 will not produce any output, as nil.to_a is no longer called.
This subtle change makes the whole splat operation more efficient since it avoids unnecessary array allocations.
Boolean operators are now allowed at the beginning of the line
Previously, multi-line conditional expressions required placing the boolean operator at the end of the preceding line, otherwise, a syntax error would occur.
if request.secret_key_base.present? &&
request.encrypted_signed_cookie_salt.present? &&
request.encrypted_cookie_salt.present?
then
do_something
endIn this new version of Ruby, you can place the boolean operator at the beginning of the line, which improves readability:
if request.secret_key_base.present?
&& request.encrypted_signed_cookie_salt.present?
&& request.encrypted_cookie_salt.present?
then
do_something
endWhat else?
- The
Arrayclass now includes its own#findmethod, an efficient override ofEnumerable#find. Additionally, an#rfindmethod has been added as an efficient alternative toarray.reverse_each.find. ZJITis becoming production-ready in Ruby 4.1. While faster than the interpreter, it has not yet surpassed YJIT.- The Ruby Jit Toolkit (RJIT) has been moved to a separate library.
You can see the full list of changes at Ruby 4.0.0-preview3.
Conclusion
Ruby core did a fantastic job with this release, bringing features that modern software development demands while maintaining the joy of programming that Ruby is known for.
The introduction of Ruby::Box opens up new architectural possibilities, and the maturation of Ractor suggests a bright future for concurrent Ruby applications.
As we count down to the final release, I encourage everyone to download this preview, test these new features, and check how they apply to your use cases. If you need assistance, Codeminer42 is here to help you navigate and leverage Ruby 4.0 to its fullest potential.
Happy coding and happy holidays!
We want to work with you. Check out our Services page!

