The letter A styled as Alchemists logo. lchemists
Published December 25, 2025 Updated January 8, 2026
Cover
Ruby 4.0.0

Happy Holidays! ❄️☃️❄️

Ruby 4.0.0 is here along with several enhancements over last year’s Ruby 3.4.0 release. The following highlights the changes in this release. Definitely dig into the release notes for complete details.

Array

Array gains two new methods:

  • #find: Provides an efficient implementation over Enumerable#find (what was previously used).

  • #rfind: Allows you to find the last element matching a specific condition which provides a performance improvement over Array#reverse_each.find by avoiding array allocation when messaging Enumerable#reverse_each. Example:

[1, 2, 3].rfind(&:even?) # 2

Binding

Methods such as local_variables, local_variable_get, and local_variable_set no longer handle numbered parameters.

Boxes

Boxes — which are a kind of Module — are a new experimental feature that allows you to work within isolated namespaces. They provide the following benefits:

  • Parallel testing and benchmarking of alternative implementations of your code.

  • Parallel blue-green deployment of your application.

  • A potential complement/alternative to Refinements.

For example, boxes give you a powerful way to benchmark two different versions of your code (which is something that was much harder to do previously):

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `benchmark`, then `chmod 755 benchmark`, and run as `./benchmark`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

One = Ruby::Box.new
One.require "one"
one = One::Demo.new

Two = Ruby::Box.new
Two.require "two"
two = Two::Demo.new

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("One") { one.call }
  benchmark.report("Two") { two.call }

  benchmark.compare!
end

The only problem is that the above will cause the following error: NameError uninitialized constant Gem: :NameTuple. Definitely more kinks to be worked out with this experimental feature.

Additionally, the Object API is awkward in that you have to initialize a box instance. I wish you could use a block for quick use. Example:

Box = Ruby::Box do |instance|
  instance.require "demo"
end

Even a keyword would be nice (granted, wishful thinking):

Box = box { |instance| instance.require "demo" }

See documentation for details.

CGI

If you were using CGI.parse to parse URIs, you’ll want to switch to Rack::Utils.parse_query since .parse has been removed.

Core

The following details the collection of changes to the core language.

The main site has been given a fresh coat of paint and is a nice upgrade compared to the previous design which was stuck in the latest 1990’s. Unfortunately, the landing page spends 50% of it’s time promoting Rails which is disappointing since Ruby is so much more than Rails.

The Ruby Object API Documentation has also been given a fresh coat of paint as well. Most significantly is better search support (including keyword shortcuts). 💡 For Alfred fans, definitely install the Pennyworth gem along with the Ruby Workflow to access Ruby resources with only a few keystrokes. 🎉

Object Allocation performance is now ~2.3 times faster for positional parameters and ~6 times faster for keyword parameters (with YJIT enabled).

A new top level Ruby module has been added (was first reserved in Ruby 3.4). This gives you access to several top level constants via Ruby.constants. Example:

%i[
  PATCHLEVEL
  Box
  REVISION
  COPYRIGHT
  ENGINE
  ENGINE_VERSION
  VERSION
  RELEASE_DATE
  DESCRIPTION
  PLATFORM
]

Performance warnings have been added for unnecessary implicit allocation of positional and keyword splats. This is also a good reminder to have Ruby Warnings enabled!

Use of *nil no longer calls nil.to_a, similar to how **nil does not call nil.to_hash. Important to be aware of especially in regards to Method Parameters And Arguments.

Using logical binary operators — ||, &&, and, and or — at the beginning of a line will be honored as the continuation of the previous line. Example:

puts "demo" if condition_a
               && condition_b
               && condition_c

Despite this addition, prefer using a single line instead of vertically stacking across multiple lines because this’ll help you identify a code smells and extract the logic to a separate method or improve the implementation to avoid using multiple conditionals altogether.

Fiber

Fiber#raise cause: was added and is similar to Kernel#fail cause:.

Fiber::Scheduler#fiber_interrupt has been added to allow a fiber to be interrupted with an exception. Initially, this is to deal with fibers waiting on a blocking I/O operation which is closed.

Kernel

Kernel#instance_variables_to_inspect — a new private method — was added as a hook to Kernel#inspect which means you can control what is exposed when inspecting an object. Example:

class Person
  def initialize name, email, address
    @name = name
    @email = email
    @address = address
  end

  private

  def instance_variables_to_inspect = %i[@name @email]
end

Person.new("Jayne Cobb", "jayne@serenity.io", "Unknown").inspect
#<Person:0x000000014e1194b0 @name="Jayne Cobb", @email="jayne@serenity.io">

Unfortunately, this only works with classes, not Struct or Data objects. Example:

Person = Data.define :name, :email, :address do
  private

  def instance_variables_to_inspect = %i[@name @email]
end

Person[name: "Jayne Cobb", email: "jayne@serenity.io", address: "Unknown"].inspect
#<data Person name="Jayne Cobb", email="jayne@serenity.io", address="Unknown">

💡 To have this work for Class, Struct, and Data — along with the ability to filter and transform what is exposed — reach for the Inspectable gem.

Pathname

Pathname is now a core class instead of a default gem. For heavy Pathname fans, this means you can delete all occurrences of require "pathname" in your implementations for a nice reduction in lines of code used. This also means one less require for the Refinements gem if you need additional Pathname firepower.

💡 Ensure Ruby Warnings are enabled to quickly catch all occurrences.

Proc

Proc#parameters has been fixed to only answer [:opt] instead of [:opt, nil] when an anonymous optional parameter is used (i.e. it and _1). This was done to improve consistency when the anonymous parameter is required.

Ractor

Stability, performance, and usability enhancements have been applied to Ractor to where it’s getting close to becoming a full fledged feature instead of being an experimental one. In this release, the Ractor Object API has been redesigned to use Ractor::Port for communicating between ractors instead of using Ractor.yield and Ractor#take (which are now removed). Example:

port_a = Ractor::Port.new
port_b = Ractor::Port.new

Ractor.new port_a, port_b do |first, second|
  first << 1
  second << 2
  first << 10
  second << 20
end

2.times { puts port_a.receive }  # 1, 10
2.times { puts port_b.receive }  # 2, 20

Set

Set is now a core class instead of an autoloaded standard library class. Additionally — and in order to make Set consistent with other core collection classes (i.e. Array and Hash) — Set#inspect now answers a string which is compatible with Set[] which can be used with eval. Example:

# Before
#<Set: {1, 2, 3}>

# After
Set[1, 2, 3]

💡 This is a powerful use of Object Inspection.

String

String has been updated to Unicode 17.0.0 and Emoji 17.0.0 (this includes Regexp being updated as well).

The following String methods now accept character selectors. Example: #strip, #strip!, #lstrip, #lstrip!, #rstrip, and #rstrip!. Previously, these methods took no arguments. Here’s an example using #strip:

"agooda".strip "a"                   # "good"
"agooda".strip "ad"                  # "goo"
"01234good56789".strip "0-9"         # "good"
"123abc57".strip("0-9", "^5", "^7")  # "abc57"

Thread

You can supply a cause as done for Kernel#fail. Example: Thread#raise cause:.

ZJIT

A new Just In Time compiler, ZJIT has been introduced to eventually eclipse YJIT. You can play with ZJIT as follows:

ruby --zjit demo.rb

Worth experimenting with further but — keep in mind — this is not meant for production yet.

Conclusion

Upgrading from Ruby 3.4.0 to 4.0.0 should be fairly seamless but definitely take the time to refactor your code and make use of the new features.