Understanding Zeitwerk in Rails 6

Marcelo Casiraghi
Cedarcode
Published in
5 min readSep 26, 2019

Zeitwerk is the new code loader engine used in Rails 6. It’s meant to be the new default for all Rails 6+ projects replacing the old classic engine.

If we judge by its features, Zeitwerk mode seems to behave pretty much the same as the previous classic mode. The basics are very similar: models, controllers, helpers and so forth will continue to be autoloaded, reloaded and eager loaded as they have always been.

However, the main difference resides in the implementation. Zeitwerk uses a better strategy for loading things, which solves all of the known gotchas that classic mode carries.

Also, something exciting is that Zeitwerk comes as a gem, which means you can leverage it inside other non-rails projects.

Why is it better?

Just like classic, Zeitwerk provides the features of code autoloading, eager loading, and reloading.

However, while classic mode relies on the const_missing callback for loading files, Zeitwerk uses Ruby’s native Module#autoload method.

Let’s explore the differences to understand why the latter is better.

How classic mode autoloads

Classic mode algorithm mainly relies on two factors: autoload_paths and the use of Ruby’s const_missing callback.

How does it work?

During code execution, every time Ruby finds an unknown constant reference a const_missing callback is fired. Rails overrides the default const_missing callback from Ruby, which would normally just raise a NameError. Instead, it performs an attempt to load the file associated with the constant being looked up.

This is when autoload_paths comes into play. Rails walks through the autoload_paths list looking for the “snake case” version of the referenced constant and, if it exists, loads/requires the file.

If everything works well, the file is required, the constant gets defined, and the code execution continues.

This works fine, but it has a few limitations.

For example, there’s a well-known issue that comes with classic mode known as When constants aren’t missed. Basically, it describes how you can easily introduce code that depends on the order in which files are autoloaded to work properly.

How so? Let’s see a quick example. Consider the following:

# app/models/user.rb
class User < ApplicationRecord
end
# app/models/admin/user.rb
module Admin
class User < ApplicationRecord
end
end
# app/models/admin/user_manager.rb
module Admin
class UserManager
def self.all
User.all # Want to load all admin users
end
end
end

In this case, if the constant Admin::User was already loaded at the time Admin::UserManager.all was called, then it would return Admin::User objects.

However, if Admin::User was not yet auto-loaded, but User was, Admin::UserManager.all would instead return User objects!

This is obviously highly undesirable, and the source of the problem comes from design: classic relies on theconst_missing callback and this callback is only fired as the last step of the lookup algorithm. So by construction, you can’t avoid having to deal with these issues at the application level.

So, how does Zeitwerk fix this? Continue reading…

Zeitwerk autoloading

Similarly to classic mode, Zeitwerk also relies on two factors. The first is the list of autoload_paths (they are actually called root directories, but they are still referenced as autoload_paths in the context of Rails).

The second factor and the main difference with classic is that Zeitwerk relies on Ruby’s native Module#autoload method for triggering the autoloading of constants.

To understand it, let’s consider the following example. Let’s say you have a Rails 6 app with the following models:

app/
models/
comment.rb
post.rb

When the project boots, Rails will call Zeitwek#setup. This method takes care of setting up the autoloaders for all of the known autoload_paths (they won’t be loaded yet though).

In the example, app/models/ directory is included in the autoload_paths, so Zeitwerk will set up a Module#autoload for each one of the constants that are inferred by the name of the files inside that directory.

In this case, Zeitwerk will infer that comment.rb and post.rb should define the constants Comment and Post respectively.

So here’s the magic; Zeitwerk will execute the following code on your behalf:

autoload :Comment, Rails.root.join('app/models/comment.rb')
autoload :Post, Rails.root.join('app/models/post.rb')

It’s as simple as that! But, what does the above do? Let’s dig into how autoload fits in this process.

Every time a Ruby program evaluates a constant there are certain steps that occur in order. For example, when Ruby evaluates the Post constant:

  • First, Ruby checks if there’s already a stored reference for :Post in the symbol table. If there’s one, it returns the pointer to the class definition.
  • If it doesn’t find one, then Ruby checks if there’s an autoload set up for :Post
  • If there’s no autoload in place, then Ruby fires a const_missing callback.
  • However, if an autoload is indeed defined (like in our example), Ruby requires the path that was passed in as an argument to the autoload call (Rails.root.join('app/models/post.rb')) and then expects the required file to define Post

And just like that, the Post constant is auto-loaded!

This works great and is a better approach than classic. Why? Because autoload is a built-in feature in Ruby, so instead of listening on const_missing and manually loading stuff (which can get hacky) we get to use the method that is served by the virtual machine to solve the very same purpose!

Note: Zeitwerk relies on the convention that each file will define the constant that is named after the name of the file (meaning that /comment.rb should define theComment constant). Luckily, this is not surprising for a normal Rails app.

Zeitwerk and Rails 6

Zeitwerk comes enabled with Rails 6 by default.

If you are upgrading, load_defaults "6.0" will set Zeitwerk as the default autoloader for your project and you’ll have it for free.

But, if for any reason you don’t want to use it, you can always opt-out by setting config.autoloader = :classic in your application.rb.

Gem usage

One of the greatest advantages of Zeitwerk is that it’s built as a separate gem from Rails. So, if you are writing or maintaining a gem, you can easily add Zeitwerk as a dependency and you’ll be able to forget about require statements!

You can read the up-to-date doc here, but mainly you just need to do:

# lib/my_gem.rb (main file)

require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!

module MyGem
# ...
end

loader.eager_load # optionally

And that’s it!

Check this sample diff from the nanoc gem when they introduced Zeitwerk to their project: https://github.com/nanoc/nanoc/pull/1403/files and look at all the code they were able to delete.

To me, this approach is highly desirable because manually writing and maintaining requires is error-prone and fragile if you don’t do it carefully. With this gem, you can just forget all about it!

Thanks to Xavier Noria and all the contributors that put the Zeitwerk project together!

--

--