blog-irya-o-que-ha-de-novo-4

10.02.2026

What's new in 4.0?

Ruby 4.0.0 was released last year (followed by a minor patch recently), and we have some changes to talk about.

Ruby 4.0.0 was released last year (followed by a minor patch recently), and we have some changes to talk about. We'll focus on new features that seem promising and show a few snippets. But, before that, there are some questions people ask when there's new version for the language they use for production apps, and we can provide at least some of the answers.

There are new minor features that won't give you much to think about, such as the logical binary operators continuing at the beginning of the line:

if cond1
&& cond2
|| cond3
...

These are always welcome, but we won't list them all here. Instead, let's focus on the bigger things that have been pointed out on the release notes.

 

New Ruby Version! Should I immediately-

No. I mean, unless you're interested in testing new features, and if that's the case, you already have the answers to your questions (at least the ones we can elucidate).

Upgrading the Ruby version for your application requires planning, enough tests to make sure everything is already working before the update, and even more testing because, after the update, things may break that you didn't (or couldn't) anticipate with prior testing. Even though the changes will probably not break your programs (there are no breaking syntax changes; only additions that makes them more readable), it is better to wait. Plan, test, do quality assurance, then upgrade, so you can do it all over again.

Even though the new features may be fun to play with, we wouldn't recommend adding them to existing apps just yet:

  • Ruby Box, for separating context, is disabled by default
  • ZJIT, an alternative JIT and successor to YJIT, is said to be slower than YJIT (for now)
  • Ractor (a helper for parallel execution) got some improvements, but it's still an experimental feature

So, take it slow, familiarize yourself with these new features, and maybe pick one or two that might be useful for your use case. But have your tests ready before anything else.

 

The box of rubies

Ruby Box is a new feature (that was called namespaces for a while) meant to provide a detached context in a Ruby process. The announcement gave us some examples of how this is expected to be used, so let's see a small example:

In `article_box.rb`:

class Array
def monkey_patch
'🐒'
end
end

def box_method
'✨'
end

$GLOBAL_VAR = '🌍'
TOP_LEVEL_VAR = '🐉'

In `main.rb`:

box = Ruby::Box.new()
box.require_relative('article_box')

monkey = box.eval('[].monkey_patch')
globe = box.eval('$GLOBAL_VAR')
top = box.eval('TOP_LEVEL_VAR')
box_method_return = box.eval('box_method')

p monkey # Will print "🐒"
p globe # Will print "🌍"
p top # Will print "🐉"
p box::TOP_LEVEL_VAR # Will print "🐉"
p box_method_return # Will print "✨"

p $GLOBAL_VAR # nil
p TOP_LEVEL_VAR # NameError
p [].monkey_patch # NoMethodError
p box::Array.new.monkey_patch # NoMethodError
p box::box_method # NoMethodError

As you can see, the monkey patch stays in the box, and you can access the defined variables, modules, classes, etc. with the box object, or evaluate strings of Ruby code and use whatever it returns (if you like to live dangerously that is, although they are useful to show these short examples without relying on multiple files and "require"-ing them, so we won't judge).

Just a reminder that the current context is not preserved when creating a new box.

require 'json'

box = Ruby::Box.new()
box.eval("{a: 5}.to_json") # NoMethodError

p({a: 5}.to_json)

You would have to `require 'json'` inside the box as well (if you need that functionality, you can still use fork).

You also can't directly call a top-level method from the box without using eval:

box = Ruby::Box.new()
box.eval('def test_method; "💯"; end')
hundred = box.eval('test_method')

p hundred # Will print "💯"
p box::test_method # NoMethodError

So, is this going to be super useful for your app? Do consider that this is still an experimental feature, requires a change in environment to work (RUBY_BOX=1), and that it will show you a nice warning:

ruby: warning: Ruby::Box is experimental, and the behavior may change in the future!

We are waiting to see some exciting uses for this. The recommendation is to play with it, perhaps make a little fun prototype if you have a good idea, and pay attention to the known issues.

 

Ractors have this name because they are Ruby Actors

Ractor is an experimental feature added back in the Ruby 3.0 release to "provide a parallel execution feature without thread-safety concerns." As of Ruby 4.0, it's still an experimental feature, but now it received a nice addition.

Previously, when working with multiple Ractors in an environment where you needed to receive some result from the Ractor, there was no way of returning it unless you pass something that can receive that result. For example, another Ractor could receive that result, kinda like a channel in Go:

def calc(n)
n.times.map { n * n }.sum
end

server = Ractor.new do
while true
n, sender = Ractor.receive
result = calc n
sender << result
end
end

server << [10, Ractor.current]

p Ractor.receive # will print 1000

You can see that there are some problems with this. Using the current Ractor will not suffice if there are more servers, and having to simulate channels by creating Ractors seems... wasteful. There is a great blog post explaining the necessity of this new implementation.

With the new ports, we can do the same example for 2 (or more) servers as simple as:

def calc(n)
n.times.map { n * n }.sum
end

port1 = Ractor::Port.new
port2 = Ractor::Port.new

server1 = Ractor.new(port1) do |port|
while true
n = Ractor.receive
value = calc(n)
port << value
end
end

server2 = Ractor.new(port2) do |port|
while true
n = Ractor.receive
value = calc(n)
port << value
end
end

server1 << 10
server2 << 20
p port1.receive # will print 1000
p port2.receive # will print 8000

In the end, there is one important thing to remember: only the Ractor that created a port (for example, the main Ractor in our case, the Ractor that every Ruby program uses by default) can call the "receive" method for that port. So we can't receive from the port that was passed as a parameter; we can only send to it. Still, it works because they have their own default ports and can receive from them (with "Ractor.receive").

Should you use it on your apps? If you're already using one of the Ruby features for concurrent programming you might want to prepare for porting them to Ractors if you find it will improve your code base. If you aren't, then, once again, play with it. It's still experimental (which means you get a warning when you use it, but at least it doesn't require an environment variable change this time), but they are planning on removing the "experimental" status this year.

 

The JITs and the Performance

Good performance is one of the most important things that people want from every programming language ever, but this time it's related to the development of a new JIT for Ruby called ZJIT. And no, ZJIT is not already done, nor is it already a replacement for the old YJIT, but it's being introduced as the "next generation" of YJIT. We can enable it easily by using "--zjit" when calling the interpreter, just like we did with YJIT.

Do we have other performance improvements? It's hard to say, since you always need to test on your app, with the kind of code you're working with, and the tiny optimizations that you did which took for granted that some things were better than others for speed or memory, at some point.

But we can at least test on something fun. The Benchmarks Game website always has some nice code for these things, and we can even compare with the previous Ruby versions. We decided to get some quick ones: spectral-norm and binary trees. We tested it on a simple notebook we have at hand (Intel Core i5-8265U, 1.60GHz), re-running each program 5 times and taking the average time spent. Also, these two don't require much I/O (no file reading necessary), so they are perfect as toy programs to test minor improvements.

Ruby Version

Tool

Program

Time (secs)

3.4.8

YJIT

spectral-norm

73.413

4.0.1

YJIT

spectral-norm

82.761

4.0.1

ZJIT

spectral-norm

95.848

3.4.8

YJIT

binary-tree

29.035

4.0.1

YJIT

binary-tree

24.987

4.0.1

ZJIT

binary-tree

38.491

Remember, this means very little for your apps, but means a lot for the fun of checking performance improvements. And from the results, what else can we say? We know that performance improvements for packages and programming languages is not a linear progression, and it's not as simple as "New version, things are faster now!", and we know that there could be new features, or checks, or whatever else they added to improve the language.

One got faster, and one got slower, even though the only change was the Ruby version. A great way to show that "your mileage may vary" and that you shouldn't take the performance changes of toy programs to evaluate a choice of upgrading your current language. We already expected ZJIT to be slower than YJIT (they told us so in the release notes), and we have some ways to go until it becomes as good as what we already have.

This is at least a good reminder to have benchmarks for those parts of your code that are known to be slow.

 

Conclusion

There is still much to wait for, but we did get some nice new things to play with. The changes to existing apps have been minor, but the promises are quite large, and once Ractor is out of the experimental phase, we'll celebrate a huge step for the language. And it's predicted to be ready this very year.

Ruby Box may take a little longer, but at least we have enough to test. The same goes for ZJIT. We are currently in the testing phase, but in a few years we'll reap those juicy performance improvements. We'll even need them to stay on par with the previous Ruby version, it seems.

stay updated

Join our community and get exclusive updates on design trends, resources, and insights delivered straight to your inbox.

enpt-br