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
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
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:
class User < ApplicationRecord
class User < ApplicationRecord
User.all # Want to load all admin users
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 was not yet auto-loaded, but
Admin::UserManager.all would instead return
This is obviously highly undesirable, and the source of the problem comes from design: classic relies on the
const_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…
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:
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
post.rb should define the constants
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
- First, Ruby checks if there’s already a stored reference for
:Postin 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
autoloadset up for
- If there’s no autoload in place, then Ruby fires a
- However, if an
autoloadis indeed defined (like in our example), Ruby
requiresthe path that was passed in as an argument to the
Rails.root.join('app/models/post.rb')) and then expects the required file to define
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.rbshould define the
Commentconstant). 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.autoload = :classic in your
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
You can read the up-to-date doc here, but mainly you just need to do:
# lib/my_gem.rb (main file)
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!
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!