Upgrading To Zeitwerk

Upgrading To Zeitwerk

Zeitwerk is the code autoloader and reloader that was integrated with Rails 6. Beginning in Rails 7, it will be the only codeloader option. As a result, upgrading to Zeitwerk will be an important step in getting your application ready for the next version of Rails. In this article, we’ll talk about upgrading your Rails 6 application from classic to zeitwerk mode.

What is Zeitwerk?

Zeitwerk opens a new window is “an efficient and thread-safe codeloader for Ruby”. It was created by Xavier Noria opens a new window to allow any Ruby project to autoload code without the numerous require statements that we, Ruby developers, have come to rely upon.

The Zeitwerk library was merged into Rails in 2019 opens a new window as part of Rails 6 to replace its previous codeloader. For older applications upgrading to Rails 6, the framework still allowed existing apps to use its “classic code” loader. However, starting in Rails 7, Zeitwerk will be the only option.

If you have been programming in Rails for a while, you will notice that your code in the app directory autoloads correctly. It has been like this for some time, and it makes development so much easier. Having our classes and modules available anywhere – without us having to explicitly add require statements or restart our development servers – helps minimize development time and improve developer experience.

This “magic” is thanks, in no small part, to code that has been packaged within Rails to automatically load and reload your source code.

Given that code loading is not broken in Rails, why did Rails 6 replace its own codeloader with Zeitwerk? To understand the rationale behind this decision, it is important to recognize what codeloaders do.

Applications need to run in different environments (eg. development, test, and production). Code loading behaviour needs to accommodate each of these environments to meet the needs for each of these environments.

For example, in development environments, we want our codeloader to watch for changes so that we do not need to reboot our app before seeing our updates.

In test environments, we may only want to selectively load the files we are testing so our tests run faster.

Finally, in production environments, we want our code to load all at once, so our application is fully available in-memory in order to improve response times.

Having a proper and robust codeloader, we can meet the needs of these different runtime environments.

Constant name lookup was also an annoyance in Rails. Before Zeitwerk, nested constant definitions were defined like this:

module Admin
  class UsersController < ApplicationController
    # ...
  end
end

After Zeitwerk, you had the option to define your nested constant namespaces like so:

class Admin::UsersController < ApplicationController
  # ...
end

While the difference may appear small, certain edge cases made autoloading more difficult, especially when things were loaded out of order. As a result, Rails chose Zeitwerk in order to handle code loading more gracefully and to simplify its internal dependencies.

Similar to Rails which relies on “convention over configuration”, Zeitwerk assumes that you define your class and module names with a certain organization.

If we follow these assumptions and rules, Zeitwerk will load the files correctly without any extra require statements:

# A hypothetical directory & subdirectory of ActiveRecord models
./app/models/website.rb           → Website
./app/models/website/setting.rb   → Website::Setting
./app/models/website/url.rb       → Website::Url
./app/models/website/url_settings → Website::UrlSettings

Here, we see two Zeitwerk specific assumptions:

  • If you place a file in a subdirectory, Zeitwerk assumes it is under its parent namespace (eg. the class definition in setting.rb must be defined as Website::Setting, as it is placed in a subdirectory of website)
  • The constant definition follows a camelcase or Pascal case style (eg. website/url_setting.rb translates to Website::UrlSetting)

In the above code example, if you wanted Website::Url to be Website::URL, you will need to define a custom inflector in the autoloader as discussed later.

Since these are already the conventions in Rails applications, your code base should already be following them. Zeitwerk follows the same conventions of this classic Rails pattern.

So what might be the difference then?

In the classic convention, we expect the filename of a file to be the underscore of the constant that’s defined within it.

With Zeitwerk, we expect the constant to be the camelize form of the filename.

# Classic
MyModule -> my_module.rb

# Zeitwerk
my_module.rb -> MyModule

In performing some Rails upgrade projects, especially ones that come originated from a much older Rails version, we sometimes see codebases not following these conventions.

We will talk about methods you can use to modify these rules to accommodate special situations in a bit to help ease these transitions. Ultimately, you want to realign your application’s codebase with Rails’ convention as these best practices are some of what makes Rails so easy and fun to use.

Upgrading Your Application to Zeitwerk Mode

To upgrade to Zeitwerk, your app needs to be running Rails 6.0 or 6.1.

At the time of writing, Rails 7.0 has not yet been released. For Rails 7 onwards, Zeitwerk is the only available codeloader. As a result, your app must be able to run in Zeitwerk mode if you plan on upgrading to Rails 7 and beyond.

Tip: If your code base is running Rails 5.x or lower, it is advisable to first upgrade to Rails 6 before proceeding to Rails 7. This will simplify your upgrade journey and minimize any issues that may come up.

If your app isn’t running in Rails 6.0 yet, you will first need to upgrade it. Need help with that? Check out our series of Rails upgrade blog posts opens a new window or reach out opens a new window to learn about our upgrade service.

When referring to “classic” mode, it simply refers to Rails’ old codeloader. For new applications, Rails will default to Zeitwerk mode, so for the remainder of this blog post, we will assume that you have upgraded all the way to Rails 6.0 or 6.1 and are running in classic mode.

Activating Zeitwerk

There are two ways to activate Zeitwerk mode:

# ./config/application.rb

config.load_defaults 6.0 # Choosing Rails 6 defaults

config.autoloader = :zeitwerk # If you need to stick with the defaults of an older Rails version, you can choose to only activate Zeitwerk separate from the Rails 6.0 defaults

If you’ve just upgraded your app to Rails 6 and do not wish to use Rails 6.0 defaults due to other reasons, you can selectively activate Zeitwerk by setting config.autoloader only.

Once this is done, Zeitwerk mode is activated.

Checking for Zeitwerk Compatibility Issues

Next, we need to check our application’s codebase for any potential issues.

You can check the status of your app’s existing code loading by typing in the following command:

bin/rails zeitwerk:check

If everything looks good, you will see a message similar to the following:

> bin/rails zeitwerk:check

Hold on, I am eager loading the application.

WARNING: The following directories will only be checked if you configure
them to be eager loaded:

  /Projects/my-app/test/mailers/previews

You may verify them manually, or add them to config.eager_load_paths
in config/application.rb and run zeitwerk:check again.

Otherwise, all is good!

What this Rake task tries to do is eager load your entire application’s code and check for errors.

If you click on the source code for that Rake task opens a new window , you will see that it calls on Zeitwerk::Loader.eager_load_all.

Internally, Zeitwerk keeps track of all the directories it needs to load and iterates through them. If Zeitwerk detects an irregularly named or defined file (ie. a file named foo.rb but defined a class called Bar inside it instead), it will raise a Zeitwerk::NameError.

Because of this reason, if your existing code deviates from this expectation, you will see the following type of error:

> bin/rails zeitwerk:check

Hold on, I am eager loading the application.
expected file app/models/user.rb to define constant User

Zeitwerk provides for you details on where it encountered the error and how to fix it.

This typically involves one or more of the following:

  1. Renaming a file, and/or
  2. Renaming a class or a module (and all references to it) to fit the camelcase rules or filename expectation, or
  3. Adding custom inflectors so Zeitwerk can properly recognize the name(s), or
  4. Configuring Zeitwerk to ignore that file altogether

Let’s go over each type of error and how they are fixed.

Fixing Upgrade Problems

1. & 2. Renaming Files & Class Definitions

One of the most common types of errors that’s seen when upgrading an application from classic to zeitwerk mode is the simultaneous misnaming of files (and constants) with extensive use of require statements.

These are easy to fix, typically by correcting the filename or renaming the class or module.

For example, let’s say you have a class called Settings. Unfortunately, when the file was created, it was named as setting.rb. Notice the singular naming in the filename. And because this file was not named according to Rails’ expectations, it would not have been autoloaded even in older Rails versions. Along with this, there’s a corresponding require statement that was added to handle the loading for this file, typically in application.rb or in an initializer.

In this case, you can rename the file to settings.rb (plural) or change the class name to Setting (singular) so that the class name and the filename match. You should also remove the corresponding require statement. You no longer need it as Zeitwerk will take care of it for you.

Let’s take a look at another example where namespacing is involved. For example, in some projects, you may have put files into a subdirectory to better organize your code.

Let’s say you have the following hierarchy:

app/
├── models/
│   ├── user.rb
│   └── user/
│       └── password.rb

In the user.rb file, the class name is User. However, in the password.rb file, you might have defined it as simply Password. Under Zeitwerk, you will need to either rename it to User::Password or move the file from the subdirectory into the “root” models directory.

You may notice that user.rb can define User and not Model::User. The reason for this is that any immediate subdirectory within the app folder is recognized as a “root” directory as it is part of the autoload path opens a new window .

3. Using Custom Inflectors For The Autoloader

In certain cases, you may need to use custom inflectors. Let’s demonstrate this with another example:

app/
├── models/
│   └── url.rb

In this case, Zeitwerk will assume that the url.rb file should be defined as Url and not URL as you might want.

To get around this, you need to let Zeitwerk know by creating an initializer and adding some custom inflection rules:

# ./config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    'url' => 'URL',
  )
end

4. Configuring Zeitwerk To Ignore Some Files

In some cases, you may have defined custom patches or have code that does not fit within Zeitwerk’s assumptions.

It is a good idea to organize them into a separate directory. Once you do that, you can add that directory to Zeitwerk’s ignore path. Note that if you do this, you will need to separately load these files with a require statement as you would have done before activating Zeitwerk.

To add a directory to Zeitwerk’s ignore path in the context of Rails, you can use the following code:

# ./config/application.rb
Rails.autoloaders.main.ignore(Rails.root.join('app/models/your/directory'))

Wrapping Up

Once you have fixed these errors, rerun bin/rails zeitwerk:check again to ensure no other NameError shows up. Repeat this until all the checks pass. Keep a list of any constants or module name changes. I recommend that you perform a codebase-wide search later to ensure that any referencing code is updated.

Remember: bin/rails zeitwerk:check simply ensures that Zeitwerk is able to eager load the entire code base when the application boots up.

If you have a test suite, you can run it now to ensure that tests pass and classes/modules are referenced properly.

Troubleshooting the Zeitwerk Upgrade

Handling Deprecations

If you have used Rails 6 previously, you may have occasionally seen the following deprecation message:

DEPRECATION WARNING: Initialization autoloaded the constant SomeClassOrModule.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

The reason is that somewhere in your code, you have autoloaded a constant or a module during Rails’ initialization. Because Rails uses an autoloader to handle all of its code loading, it’s important that constant definitions should not happen during the initialization as this should only be handled by the code loader.

To fix this, you can wrap your code with the following:

# Typically in an initializer or application.rb
Rails.application.config.to_prepare do
  # Your code here
end

Your code must be able to be run with idempotency. In other words, Rails may very well run the code you place within the block multiple times, so make sure that your code logic supports this possibility.

Root Namespacing With Zeitwerk

Sometimes, your legacy application may have some namespacing requirements, simply due to the way it was configured with an older Rails version.

Let’s start with an example:

app/
├── services/
│   └── authentication.rb
│   └── payment.rb
│   └── verification.rb

In the above directory structure, you may have defined the classes as Services::Authentication, Services::Payment, and Services::Verification. Correspondingly, the directory ./app/services was added to the app’s load path in application.rb or in an initializer.

While this worked previously in classic mode, Zeitwerk will interpret the subdirectory of services as a “root” directory. This means that in order for you to keep your current namespacing, you will need to add some additional configuration or move the files under another level of subdirectory like so:

app/
├── services/
│   └── services/
│       └── authentication.rb
│       └── payment.rb
│       └── verification.rb

Of course, nesting it under another subdirectory level like this may look confusing, especially if the codebase is worked on by larger teams.

Here is an alternative solution for this problem.

First, remove the subdirectory from Zeitwerk’s autoload paths, and then configure Zeitwerk to interpret the app directory as a root directory:

# ./config/application.rb
config.eager_load_paths.delete("#{Rails.root}/app/services")
config.eager_load_paths.unshift("#{Rails.root}/app")

What this will do is make sure that Zeitwerk interprets ./app/services in a non-root fashion, and so files placed under ./app/services will be expected to be namespaced properly (eg. Services::YourClass).

Notes:

Application Fails To Boot

Now that you’ve upgraded your app, it is time to deploy.

Occasionally, I’ve encountered situations where the application does not boot up when it’s deployed, even though the test suite passes. If you’ve added test coverage for all the implementation code, this should never happen.

Typically, the reason this issue comes up is because you have created or renamed a file, but its corresponding class or module name definition does not match. For example, let’s say you have several classes that are similar, and you decided to duplicate the files in order to create new classes. During the duplication, you had forgotten to rename the corresponding class name. As well, because you were in a rush, the test coverage was minimal and did not catch this.

In the development and test environments, Zeitwerk only loads what it needs to load. In other words, it does not eager load the entire application. However, in production environments, Zeitwerk’s eager loading will be called by Rails. This is configured with the config.eager_load configuration in the environment specific files.

Due to the test environment not fully loading your files, this misnamed class does not crash your application in either the test or development environments.

A good way to check for this is to rerun bin/rails zeitwerk:check. You can also add a quick test to your test suite opens a new window to automatically check for this as well.

Final Words

Zeitwerk’s autoloader simplifies not only the code loading of our app, but also the organization of our source files and the class definitions themselves. When upgrading older applications, the best way to think of this is that you are aligning your code’s naming with Rails’ philosophy of “convention over configuration”.

In the past, we developers have utilized various loading techniques which tend to be brittle. When choosing to either implement some custom naming (such as through deleting an autoload path) or to simply defer to the choices made by the Rails framework, it is best to defer to the framework’s choices.

This may mean renaming your existing files and performing code wide search and replace as part of the upgrade clean up process. The long term gain in doing this is that it helps code maintainers work within a similar set of naming conventions and make it easier for new developers to be onboarded.

Good luck with the upgrade!

Further Reading

Acknowledgements

Thank you to Xavier Noria opens a new window for his help in reviewing this post.

Get the book