Blog
0 pixels scrolled
  • Home
  • Work
  • Services
  • About
  • Blog
  • Contact
Debugging The Zeitwerk Migration
Senem Soy on February 27, 2024
Struggling with the switch to Zeitwerk? Learn about how autoloading works in Ruby and Rails and some tips and tricks to help debug this migration.
Debugging The Zeitwerk Migration
Senem Soy on February 27, 2024
Posted in Rails
← Back to the blog

Rails 6 introduces a new way of autoloading, integrating the gem Zeitwerk. Rails 7 drops support for classic autoloading, so it’s necessary to switch to Zeitwerk if you ever want to upgrade to Rails 7.

For those not familiar: autoloading in Rails provides access to all your classes and modules automatically, without having to use require. Making the switch to Zeitwerk-based autoloading can be tricky, because it changes how your application looks up source code files. It’s easy to run into problems if you’re not aware of Zeitwerk’s conventions, and how it differs from the classic autoloader.

How are constants resolved in Ruby

module Admin
  class ProductsController < ApplicationController
    def new
      @product = Product.new
    end
  end
end

In this example; we’re in the Admin::ProductsController namespace and we want to create a new Product. To do so, we want to find the Product constant and we start looking for it in the namespace we are in. Product constant, does it belong to Admin::ProductsController? No, it’s not found there, so we check the nesting upwards. Does Admin have a Product constant? It does not and now we’ve exhausted the nesting chain.

The search continues up the ancestor chain, where the superclass of Admin::ProductsController is ApplicationController. Does ApplicationController have a Product constant? It does not so the search continues upwards the ancestor chain, till we eventually get to Object. Top level classes are all objects so Ruby will be able to find the constant Product now.

How does the classic autoloader work in Rails

Rails’ classic autoloader works by incorporating file name conventions, autoload paths, Module#const_missing. The const_missing hook is called whenever a constant we’re trying to use is missing. From the Product example, the name Product would get passed to const_missing. With that name, the const_missing hook starts looking for the lowercase name in the autoload paths. Once it finds the file, it loads and everything works.

Autoload method in Ruby

To understand how Zeitwerk works; we need to first understand the Ruby autoload method.

autoload :Product, 'product'

In the example above, autoload will load the file product to load the :Product constant. When Ruby is trying to resolve a constant (checking modules and classes), if it does not find the constant, but there is an autoload defined for that constant, it executes the autoload. This happens at the right place at the right time, instead of waiting for the const_missing hook to be called like in the classic autoloader.

How Zeitwerk works

Zeitwerk utilizes this autoload method while sticking to the file name conventions. Instead of reacting to const_missing, Zeitwerk scans the project tree before the execution of the application. If there is a product.rb in the file system, it defines an autoloader for that particular file: autoload :Product, 'product'.

How to switch to Zeitwerk

Rails 6 or higher is required to be able to use Zeitwerk. All you have to do is tell Rails not to use the classic autoloader anymore.

# config/application.rb
config.load_defaults 6.0
config.autoloader = :classic # DELETE THIS LINE

Debugging

The first step to ensuring Zeitwerk works properly with your application is running the zeitwerk:check command. What this does is to eager load the application and raise an error if there is a missing constant in autoload paths.

"expected file app/models/product.rb to define constant Product"

The raised error tells us that it can’t find the constant Product where it should be, product.rb. Making sure all your modules and classes adhere to the Rails file name conventions should handle these errors. Repeat this command till you get all clear, but that doesn’t mean that you still won’t have runtime errors.

Check what is being autoloaded before and after Zeitwerk

One trick I found very helpful in debugging runtime issues that pop up after switching to Zeitwerk is checking to see what constants are actually being autoloaded. I did this by first putting Rails.autoloader.log! in config/application.rb. Then I ran a rails server and saved the log into a file and committed it. I went back to a commit where I wasn’t having issues (pre-Zeitwerk) and ran the Rails.autoloader.log! command again and copy-pasted that on the file where I had committed the initial logs. The diff that comes up once you save the file will show what constants are not being autoloaded anymore after switching to Zeitwerk.

A screenshot of what my diff looked like

Debugging decorator loading

Another thing I found was calling the ancestors method on a constant. Let’s say we have a ProductDecorator, but we also use an extension called Snowball that is also decorating the same Product class in Snowball::ProductDecorator. Calling MyApp::Product.ancestors in a Rails console would be very beneficial at this point:

[1] pry(main)> MyApp::Product.ancestors
=> [Snowball::ProductDecorator,
MyApp::Product,
...
Kernel,
BasicObject]

Here I see that the ProductDecorator in my application isn’t being loaded for some reason and I can start to investigate why Zeitwerk is having problems with that decorator file.

[1] pry(main)> MyApp::Product.ancestors
=> [Snowball::ProductDecorator,
MyApp::ProductDecorator,
MyApp::Product,
...
Kernel,
BasicObject]

This time, the decorator from my application is being loaded, but not in the order that I want. The extension’s decorator is higher on the ancestral chain than my application’s decorator, which is telling me something might be double loading and causing problems.

In conclusion, switching from the classic autoloader to Zeitwerk can be a complex process due to their fundamental differences. It’s crucial to understand how constants are resolved in Ruby, how the classic autoloader works, and how Zeitwerk works for a successful migration. I hope the debugging methods I’ve shared here helps your migration go smooth!

Work ServicesAboutBlogCareersContact


Privacy policyTerms and conditions© 2024 Super Good Software Inc. All rights reserved.