Ruby super( ) annoying, Ruby still awesome

3 min read

TLDR

  • Use super when you want to call the parent with all of the current function’s arguments
  • Use super() when you want to call the parent with none of the current function’s arguments
  • Use super(arg1, arg2) when you want to call the parent with specific arguments or when none of the current function’s arguments are being accepted for some reason

Ruby super( ) annoying

Coding is hard, though some say I make it look easy. Here’s a fun Ruby surprise I experienced recently.

This is not exactly how the classes looked, but we had a base class like the following:

class Acme::Base
  def initialize
    @auth_token = ENV['ACME_AUTH_TOKEN'] # Used later in a private method.
  end

  def create
    # Make POST request to endpoint using auth token...
  end
end

And we had some sub classes that inherited from this base class:

class Acme::Endpoint1 < Acme::Base
  def initialize(some_input)
    @some_input = some_input # Different input for each sub class.
  end

  def url
    '/endpoint_1'
  end
end

class Acme::Endpoint2 < Acme::Base
  # Same as above but with /endpoint_2
end

class Acme::Endpoint3 < Acme::Base
  # Same as above but with /endpoint_3
end

I needed to be able to use a different auth token sometimes with something like:

# Existing behaviour.
Acme::Endpoint1.new(some_input).create

# New behaviour.
token = ENV['ACME_OTHER_AUTH_TOKEN']
Acme::Endpoint1.new(some_input, auth_token: token).create

So I decided to change initialize in the Base and Endpoint classes like this:

class Acme::Base
  def initialize(auth_token: ENV['ACME_AUTH_TOKEN'])
    @auth_token = auth_token
  end
end

class Acme::Endpoint1 < Acme::Base
  def initialize(some_input, auth_token: ENV['ACME_AUTH_TOKEN'])
    super(auth_token: auth_token)
  end
end

Notice I added (auth_token: ENV['ACME_AUTH_TOKEN']) to these initialize methods, I set the @auth_token instance variable in the base class, and I call super in the Endpoint1 sub class.

This way I can pass in an auth_token or use ENV['ACME_AUTH_TOKEN'] as the default.

All the tests passed so I pushed to production.

The next day I was notified that the all the requests to Endpoint2 and Endpoint3 were failing because they were missing the auth token. I knew right away where the problem was. I had a fun hour and a half figuring why the problem was.

I thought, “obviously I have to call the super function in the other base classes”, so I added this:

class Acme::Endpoint2 < Acme::Base
  def initialize(some_input)
    super
  end
end

class Acme::Endpoint3 < Acme::Base
  def initialize(some_input)
    super
  end
end

I ran the tests and I got:

ArgumentError: wrong number of arguments (given 1, expected 0)

This is because Acme::Base#initialize is expecting keyword arguments but for some reason it’s receiving positional arguments.

I googled. I found this StackOverflow post about super and updated it to this:

class Acme::Endpoint2 < Acme::Base
  def initialize(some_input)
    super()
  end
end

class Acme::Endpoint3 < Acme::Base
  def initialize(some_input)
    super()
  end
end

Notice that I added parenthesis to super. Now we’re telling ruby not to pass some_input as an argument to the super method. In Ruby, you don’t usually have closed parentheses for methods that are not receiving arguments. This is not how I expected it to work.

I even added some extra tests that fail without super(). All tests passed. This time I also tested Endpoint2 and Endpoint3 in our pre-production environment.

I pushed to production. They still failed.

I thought this is completely illogical. My tests pass locally and in our continuous integration server. I thought something must be wrong somewhere other than my code. I already spent too much time on this. Time to brute force:

class Acme::Endpoint2 < Acme::Base
  def initialize(some_input)
    super(auth_token: ENV['ACME_AUTH_TOKEN'])
  end
end

class Acme::Endpoint3 < Acme::Base
  def initialize(some_input)
    super(auth_token: ENV['ACME_AUTH_TOKEN'])
  end
end

Yup. I shoved the auth_token: ENV['ACME_AUTH_TOKEN'] into super. Works in pre-production, works in production.

I still don’t know why this didn’t work in production but the brute force approach worked so I’m moving on with my life. I already spent an hour and a half solving this, and now another hour writing this post.

I’ve been using Ruby since 2015 and I believe this is the first time I’ve encountered this situation. It was “super” annoying.

Every language has its quirks and I still like Ruby. I generally favour composition over inheritance but this was a new low.

Moral of the story: Avoid Inheritance unless it’s money from your family.

Written on March 7, 2023

The opinions expressed here are my own and do not reflect any individual or organization from my past or present.

Subscribe

My newsletter will inspire you and boost your software engineering skills - subscribe now!