Introduction

Ruby 3.0 changed how keyword arguments are handled, separating them from positional hash arguments. Code that passed a hash as the last argument to a method expecting keyword arguments worked in Ruby 2.7 but raises ArgumentError in Ruby 3.0. This is one of the most impactful breaking changes in Ruby's recent history, affecting applications and gems alike.

Symptoms

  • ArgumentError: wrong number of arguments (given 1, expected 0) after Ruby 3 upgrade
  • ArgumentError: unknown keyword: :some_key
  • Deprecation warnings in Ruby 2.7 that became errors in Ruby 3.0
  • Works with RUBYOPT='-W:no-deprecated' flag in Ruby 2.7 but fails in 3.0
  • Third-party gems crash with ArgumentError on method calls

Example error: ```ruby # In Ruby 2.7, this worked: def create_user(name:, email:, role:) User.create(name: name, email: email, role: role) end

params = { name: 'John', email: 'john@example.com', role: 'admin' } create_user(params) # Works in 2.7, fails in 3.0

# Ruby 3.0 error: ArgumentError: wrong number of arguments (given 1, expected 0) Did you mean? create_user(name: 'John', email: 'john@example.com', role: 'admin') ```

Common Causes

  • Passing a hash as last positional argument to a method with keyword parameters
  • Using **hash to forward arguments to methods with different signatures
  • Delegation methods that use *args, &block without **kwargs
  • Gems not yet updated for Ruby 3 compatibility
  • Using send or public_send with keyword arguments as a hash

Step-by-Step Fix

  1. 1.Use the double splat operator to convert hash to keywords:
  2. 2.```ruby
  3. 3.# Before (Ruby 2.7 style)
  4. 4.create_user(params)

# After (Ruby 3 style) create_user(**params) ```

  1. 1.Fix delegation methods to forward keywords properly:
  2. 2.```ruby
  3. 3.# Before (Ruby 2.7)
  4. 4.def delegate_method(*args, &block)
  5. 5.target.send(:method, *args, &block)
  6. 6.end

# After (Ruby 3) def delegate_method(*args, **kwargs, &block) target.send(:method, *args, **kwargs, &block) end

# Or use Ruby 3.1+ argument forwarding: def delegate_method(...) target.send(:method, ...) end ```

  1. 1.Handle methods that accept both hash and keywords:
  2. 2.```ruby
  3. 3.# If the method needs to accept both styles:
  4. 4.def create_user(options = nil, **kwargs)
  5. 5.params = options.is_a?(Hash) ? options.merge(kwargs) : kwargs
  6. 6.User.create(params)
  7. 7.end

# Or explicitly require keywords: def create_user(name:, email:, role: 'user') User.create(name: name, email: email, role: role) end ```

  1. 1.Check gem compatibility before upgrading:
  2. 2.```bash
  3. 3.# Use ruby-next to scan for compatibility issues
  4. 4.gem install ruby-next
  5. 5.ruby-next translate . --check

# Or run with verbose warnings in Ruby 2.7 first RUBYOPT='-W:deprecated' bundle exec rspec ```

  1. 1.Update Gemfile for Ruby 3 compatibility:
  2. 2.```ruby
  3. 3.ruby '3.2.2'

# Update gems known to have Ruby 3 issues gem 'rails', '~> 7.1' gem 'sidekiq', '~> 7.2' gem 'devise', '~> 4.9' ```

Prevention

  • Run your test suite with Ruby 3.0+ before upgrading production
  • Use ruby-next gem to find and auto-fix keyword argument issues
  • Update all major gems before upgrading Ruby version
  • Add Ruby version to your CI matrix to catch compatibility issues early
  • Document Ruby version requirements in your project README
  • Use ... argument forwarding (Ruby 2.7+) for delegation methods