Rails 5, Require inexistant class fallback

Hello !

I found a behaviour that seems like a bug and couldn’t find discussion about it. In a rails app (with or without bootsnap) with the modules : A::B and A::C::E, in rails console, if I use A::C::B (does not exist) it will fallback to A::B once, every next use of A::C::B will raise an error.

edit : I could not repro in rails 6

repro : https://github.com/Mziserman/repro,

in rails console :

irb(main):001:0> User::Smth::UpdateTransaction.new.call
=> User::UpdateTransaction
irb(main):002:0> User::Smth::UpdateTransaction.new.call
Traceback (most recent call last):
        1: from (irb):2
NameError (uninitialized constant User::Smth::UpdateTransaction)

This is a fundamental limitation of the classic autoloader which is due to the fact that the needed resolution algorithm and nesting are unknown, it is documented here in the case of a relative constant. Your case is not the same, but similar in essence, A::B is loaded when it shouldn’t because the classic autoloader does not have enough information to understand it should not autoload that one.

In this talk I cover why does the classic autoloader have these limitations.

Why does it err in the second evaluation? The classic autoloader has a heuristic that basically says “if I am looking for A::C::B and I see that A::B exists, then Ruby would have resolved the constant if relative and I would have not been called in the first place, therefore the missing constant must be absolute”, and it errs. That explains the error raised in the second evaluation. In my view, this is an attempt for the classic autoloader to be as precise as possible, but one that is confusing. You can’t be precise in general, and in this case it depends on load order. A debatable feature in my view, but it does what you’d expect in other scenarios, so it is a trade-off.

These gotchas and others are solved by the new zeitwerk mode in Rails 6.

2 Likes