The traditional autoloader in Rails was quite helpful, but it had a few flaws that occasionally made autoloading challenging. To overcome the challenges, Xavier Noria introduced zeitwerk mode in Rails 6 with this PR and kept it configurable. Rails 7 onwards, the zeitwerk completely replaces the classic autoloader.
In this article, we'll look at some gotchas in classic autoloading and how Zeitwerk mode solves them.
(You may read this article to learn and understand how Rails autoloading works).
How classic autoloading works?
From the start, Rails used an autoloader implemented in Active Support called Classic Autoloading which is available till Rails 6.
Classic Autoloading depends on the Ruby constant lookup. To resolve a constant, it is first searched in the lexical scope of the defined class and then in its ancestors. If the constant is not found, the const_missing method is called by Ruby. Rails overrides Ruby's const_missing method and uses the autoload_paths to resolve the constant as per the convention.
How zeitwerk autoloading works?
The newly introduced Zeitwerk Mode does not depend on Ruby's constant lookup.
Instead, it makes use of Ruby's Module#autoload method to instruct Ruby ahead of time which file will define a specific constant without incurring the cost of loading that file right away.

Common Problems resolved by zeitwerk mode
Various issues were present with the classic mode and have been resolved by the zeitwerk mode. Out of them, we'll look at three different gotchas, each with an example.
-
When Constants aren't Missed
Let's consider we have the following model structure.# course.rb class Course def initialize puts "From Course" end end # mit_university/course.rb module MitUniversity class Course def initialize puts "From MitUniversity::Course" end end end # mit_university/engineering.rb module MitUniversity class Engineering def initialize @course = Course.new end end endWith Classic Mode
Loading development environment (Rails 5.2.7.1) 2.7.5 :001 > Course.new From Course => #<Course:0x0000563bfa029810> 2.7.5 :002 > MitUniversity::Engineering.new From Course => #<MitUniversity::Engineering:0x0000563bf9e7ab40 @course=#<Course:0x0000563bf9e7aaf0>>Here, as we have called
Coursebefore callingMitUniversity::Course, Ruby's constant lookup autoloadedCoursein memory.
So if we try to create an object forMitUniversity::Engineering, it refers to theCoursewhich was already autoloaded in memory instead of searching forMitUniversity::Course.
This makes autoloading dependent on the order in which constants are called.With Zeitwerk Mode
Loading development environment (Rails 7.0.3) 2.7.5 :001 > Course.new From Course => #<Course:0x00005615a9707fa0> 2.7.5 :002 > MitUniversity::Engineering.new From MitUniversity::Course => #<MitUniversity::Engineering:0x00005615ae41d290 @course=#<MitUniversity::Course:0x00005615ae40b270>> 2.7.5 :003 >As zeitwerk mode defines
autoload_pathfor all the constants, it already knows where to look for which constant. So despite initializingCoursefirst, it still loadsMitUniversity::Courseas expected when called fromMitUniversity::Engineeringclass. -
Autoloading within Singleton Classes
A similar issue has been resolved for the Singleton class method using zeitwerk mode. An example is shown below:# mit_university/course.rb module MitUniversity class Course def initialize puts "From MitUniversity::Course" end end end # mit_university/engineering.rb module MitUniversity class Engineering class << self def details Course.new end end end endWith classic mode
If we call
MitUniversity::Engineering.detailsbefore callingMitUniversity::Courseit will throw an erroruninitialized constant Course. This is because, when autoloading is triggered, Rails only checks the top-level namespace as the singleton class is anonymous and does not know about the nesting ofMitUniversity.Loading development environment (Rails 5.2.7.1) 2.7.5 :001 > MitUniversity::Engineering.details Traceback (most recent call last): 2: from (irb):3 1: from app/models/mit_university/engineering.rb:5:in `details' NameError (uninitialized constant Course) 2.7.5 :002 > MitUniversity::Course => MitUniversity::Course 2.7.5 :003 > MitUniversity::Engineering.details From MitUniversity::Course => #<MitUniversity::Course:0x0000560cabb27c18>With zeitwerk mode
Zeitwerk does not throw any error and loads the constant even if it was not autoloaded before.
Loading development environment (Rails 7.0.3) 2.7.5 :001 > MitUniversity::Engineering.details From MitUniversity::Course => #<MitUniversity::Course:0x0000559ebba13dd0> -
Autoloading and Single-table Inheritance (STI)
Let us say, we have the following Single-table Inheritance (STI) models defined.class Polygon < ApplicationRecord end class Triangle < Polygon end class Rectangle < Polygon end class Square < Rectangle endSquareinherits fromRectangle, so when we callRectangle.allthe result must includePolygonof typeSquarealong withRectangle.With classic mode
However, when we call
Rectangle.allwe do not seeSquarerecords in the result. We can see the generated SQL query does not includeSquare.Loading development environment (Rails 5.2.7.1) 2.7.5 :001 > Rectangle.all Rectangle Load (0.4ms) SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` = 'Rectangle' /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">]> 2.7.5 :002 > Square => Square(id: integer, area: float, type: string, type_id: integer, created_at: datetime, updated_at: datetime) 2.7.5 :003 > Rectangle.all Rectangle Load (0.9ms) SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">, #<Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000">]>To resolve this, we have to add
require_dependency 'square'at the bottom of therectangle.rbfile.# app/models/rectangle.rb class Rectangle < Polygon end require_dependency 'square'With zeitwerk mode
Since in zeitwerk mode
Squaregets autoloaded we don't need to addrequire_dependency 'square'.Loading development environment (Rails 7.0.3) 2.7.5 :001 > Rectangle.all Rectangle Load (0.9ms) SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">, #<Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000">]>
Conclusion
With Rails 7, Zeitwerk has become the default mode and classic mode is no longer available. This is an impactful change that has improved the way constants are autoloaded in Rails and has resolved many more issues with classic autoloading apart from the ones we have covered above.