Rails `NameError: uninitialized constant` only in production (not console) - class is in /app subfolder - needs to add double colon `::`

Viewed 439

In a Rails 5.2 app, I have a class Foos::SomethingSerializer, stored in app/serializers/foos/something_serializer.rb.

In Development, this controller code works just fine:

# api/v1/foos_controller.rb

  render json: Foos::SomethingSerializer.new(foo).as_json

In Production only, this code raises an exception NameError: uninitialized constant Api::V1::Foos::SomethingSerializer

I've had this before so I just added :: in front of it, like:

# api/v1/foos_controller.rb

  render json: ::Foos::SomethingSerializer.new(foo).as_json

I could call it a day, but I would really like to understand this, since these bugs that only bites in Production are the worst.

Also, Rails.configuration.eager_load_paths in the console shows that /app/serializers is on the autoload path, as it should be, as it's a subfolder of app/.

Also, in the production environment console, I can type Foos::SomethingSerializer.new without any exception whatsoever (so it finds the constant).

So why (1) in development this works and (2) in production it throws an exception trying to find that constant namespaced inside the controller and can't find it on the upper Foos namespace?

1 Answers

Ruby has relative constant lookup.

module A
  class B
    def self.c
      C # note that it's not A::C
    end  
  end  
end  

module A
  class C
  end
end

class C
end

C # C < Object
A::B.c # A::C < Object

If it can find a constant relatively to where you're looking from, it will return that. If it cannot find the constant in the current context, it will go up a level and look for it there until it reaches root level. If the constant is not found on root level, then an error is raised.

Prepending :: to the constant tells ruby to just start looking from the root level.

In your code, you have both ::Foos and Api::V1::Foos defined. When calling Foos::SomethingSerializer from inside Api::V1, ruby will find Api::V1::Foos instead of ::Foos, but there is no SomethingSerializer there, so it will fail. I know it's inconvenient and I don't personally like it either, but that's how it is. If you know you're looking for something from the root level, adding :: to the start is always safe.

It didn't fail on development, because you have autoloading on. When you called Foos::SomethingSerializer, Api::V1::Foos was not loaded yet and since ::Foos was already loaded, rails found it and didn't try to autoload Api::V1::Foos.

I suggest you run your specs with eager_load = true to attempt to catch errors like this and use root-level lookup (::) whenever you have constants with the same name in both root and nested contexts.

Related