info:To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Ruby 3 gotchas
This section documents several problems we found while working on [Ruby 3 support](https://gitlab.com/groups/gitlab-org/-/epics/5149)
and which led to subtle bugs or test failures that were difficult to understand. We encourage every GitLab contributor
who writes Ruby code on a regular basis to familiarize themselves with these issues.
The complete list of changes to the Ruby 3 language and standard library is found [here](https://rubyreferences.github.io/rubychanges/3.0.html).
## `Hash#each` consistently yields a 2-element array to lambdas
Consider the following code snippet:
```ruby
deffoo(a,b)
p[a,b]
end
defbar(a,b=2)
p[a,b]
end
foo_lambda=method(:foo).to_proc
bar_lambda=method(:bar).to_proc
{a: 1}.each(&foo_lambda)
{a: 1}.each(&bar_lambda)
```
In Ruby 2.7, the output of this program suggests that yielding hash entries to lambdas behaves
differently depending on how many required arguments there are:
```ruby
# Ruby 2.7
{a: 1}.each(&foo_lambda)# prints [:a, 1]
{a: 1}.each(&bar_lambda)# prints [[:a, 1], 2]
```
Ruby 3 makes this behavior consistent and always attempts to yield hash entries as a single `[key, value]` array:
```ruby
# Ruby 3.0
{a: 1}.each(&foo_lambda)# `foo': wrong number of arguments (given 1, expected 2) (ArgumentError)
{a: 1}.each(&bar_lambda)# prints [[:a, 1], 2]
```
To write code that works under both 2.7 and 3.0, consider the following options:
- Always pass the lambda body as a block: `{ a: 1 }.each { |a, b| p [a, b] }`.
- Deconstruct the lambda arguments: `{ a: 1 }.each(&->((a, b)) { p [a, b] })`.
We recommend always passing the block explicitly, and prefer two required arguments as block parameters.
More information can be found in [Ruby issue 12706](https://bugs.ruby-lang.org/issues/12706).
## `Symbol#to_proc` returns signature metadata consistent with lambdas
A common idiom in Ruby is to obtain procs via the `&:<symbol>` shorthand and
pass them to higher-order functions:
```ruby
[1,2,3].each(&:to_s)
```
Ruby desugars `&:<symbol>` to `Symbol#to_proc`, which we can `call` with
the method _receiver_ as its first argument (here `Integer`), and all method _arguments_
(here none) as its remaining arguments.
This behaves the same in both Ruby 2.7 and Ruby 3; where Ruby 3 diverges is when capturing
this proc and inspecting its call signature.
This is often done when writing DSLs or using other forms of meta-programming:
```ruby
p=:foo.to_proc# This usually happens via a conversion through `&:foo`
# Ruby 2.7: prints [[:rest]] (-1)
# Ruby 3.0: prints [[:req], [:rest]] (-2)
puts"#{p.parameters} (#{p.arity})"
```
Ruby 2.7 reports zero required and one optional parameter for this proc, while Ruby 3 reports one required
and one optional parameter. As described above, Ruby 2.7 is incorrect: the first argument must
always be passed, as it is the receiver of the method the proc represents, and methods cannot be
called without a receiver.
Ruby 3 corrects this, meaning code that tests proc arity or parameter lists might now break and
has to be updated.
More information can be found in [Ruby issue 16260](https://bugs.ruby-lang.org/issues/16260).
## `OpenStruct` does not evaluate fields lazily
The `OpenStruct` implementation has undergone a partial rewrite in Ruby 3, resulting in
behavioral changes. In Ruby 2.7, `OpenStruct` defines methods lazily, when the method is first accessed.
In Ruby 3.0, it defines these methods eagerly in the initializer, which can break classes that inherit from `OpenStruct`
and override these methods.
Don't inherit from `OpenStruct` for these reasons; ideally, don't use it at all.
`OpenStruct` is [considered problematic](https://ruby-doc.org/stdlib-3.0.2/libdoc/ostruct/rdoc/OpenStruct.html#class-OpenStruct-label-Caveats) for various reasons. When writing new code, prefer a `Struct` instead, which is simpler in implementation, although less flexible.
## `Regexp` and `Range` instances are frozen
It is not necessary anymore to explicitly `freeze``Regexp` or `Range` instances, since Ruby 3 freezes
them automatically upon creation.
This has a subtle side-effect: Tests that stub method calls on these types now fail with an error, since