Rails Zeitwerk Autoloading: NameErrors oplossen, Eager Loading en de Classic Loader Migratie
Rails Zeitwerk autoloading uitgelegd: fix NameErrors, begrijp eager loading valkuilen, migreer van Classic loader, en configureer inflections. Volledige gids.
Het berichtje kwam op een donderdagochtend om twee uur. Niet van een klant, maar van een Sentry-alert op mijn telefoon, veertig seconden na een geslaagde GitHub Actions-deploy: NameError: uninitialized constant Api::V2::PaymentGateway::StripeAdapter.
Die constante bestond. Ik had hem twee weken eerder geschreven. De volledige testsuite op CI was groen. Staging had nooit geklaagd. We hadden de rollback in vier minuten gedaan. Nog twee uur later snapte ik eindelijk waarom het mis was gegaan.
Ontwikkelomgevingen laden constanten lazy, bij eerste gebruik. Productie laadt alles in één keer bij het opstarten. Het StripeAdapter-bestand had een subtiele naamgevingsfout die Zeitwerk tolereerde onder lazy loading — omdat iets anders het bestand toevallig eerder inlaadde — maar die hij afkeurde bij eager loading. We hadden een productie-NameError opgeleverd die geen enkele test had gevangen, omdat ook onze CI-omgeving met eager loading uitgeschakeld draaide.
Na negentien jaar Rails heb ik deze fout zelf gemaakt, bij klanten zien gebeuren, en Zeitwerks regels vaker uitgelegd dan ik zou willen. Dit is de gids die ik wilde hebben voor die donderdagnacht.
Hoe Rails Zeitwerk Autoloading Werkt
Zeitwerk verving Rails’ Classic autoloader in Rails 6.0 en werd de enige ondersteunde optie vanaf Rails 7.0. Classic autoloading gebruikte $LOAD_PATH-lookups en const_missing-hooks — flexibel op manieren die subtiele bugs veroorzaakten in multithreaded omgevingen, en fundamenteel incompatibel met thread-safe eager loading.
Zeitwerk is strikter en voorspelbaarder. De kernregel: het bestandspad relatief aan een autoload-root moet exact overeenkomen met de constante die het definieert.
app/models/user.rb verwacht dat het bestand User definieert. app/models/admin/billing_report.rb verwacht Admin::BillingReport. Één bestand, één constante, het pad dicteert de naam.
Ontwikkelingsmodus laadt lazy: constanten worden ingeladen bij eerste gebruik. Productimodus laadt eager: alle bestanden onder autoload-roots worden bij het opstarten ingeladen. Dat verschil is waar de meeste Zeitwerk-bugs zich verstoppen. Een constante die in development werkt omdat hij toevallig eerder ingeladen wordt via een andere route — een keten die Zeitwerk niet kan garanderen — werkt stil in development maar faalt in productie.
De NameErrors Die Je Echt Gaat Tegenkomen
Bestandsnaam en constante komen niet overeen
# app/services/stripe_payment_processor.rb
class StripePaymentProcessorService # FOUT
def charge(amount:, currency:)
# ...
end
end
Zeitwerk verwacht StripePaymentProcessor in dit bestand omdat het stripe_payment_processor.rb heet. Wanneer code elders StripePaymentProcessorService aanroept, zoekt Zeitwerk naar stripe_payment_processor_service.rb, dat niet bestaat, en gooit een NameError.
Oplossing: hernoem het bestand naar stripe_payment_processor_service.rb, of hernoem de klasse naar StripePaymentProcessor. Kies wat overeenkomt met hoe de rest van je codebase ernaar verwijst. Voer daarna de checker uit:
bin/rails zeitwerk:check
Dit scant alle autoload-paden en rapporteert elk bestand waar de gedefinieerde constante niet overeenkomt met wat Zeitwerk op basis van de bestandsnaam verwacht. Voeg dit toe aan je CI-pipeline. Het duurt minder dan twee seconden.
Naamruimtestructuur komt niet overeen met mapstructuur
# app/models/billing_invoice.rb ← verkeerde locatie
class Billing::Invoice # Zeitwerk verwacht dit in app/models/billing/invoice.rb
end
Zeitwerk leest het pad om de verwachte constante te bepalen. billing_invoice.rb in de root van app/models/ vertelt Zeitwerk dat BillingInvoice verwacht wordt. Onder lazy loading gaat dit mis zodra eager loading aanstaat. Zeitwerk verwerkt billing_invoice.rb, verwacht BillingInvoice, vindt Billing::Invoice en gooit een Zeitwerk::Error.
De oplossing is om je modulehiërarchie te spiegelen in de mapstructuur:
app/models/
billing.rb # definieert module Billing (of laat weg — Zeitwerk maakt de module aan)
billing/
invoice.rb # definieert Billing::Invoice
line_item.rb # definieert Billing::LineItem
Zeitwerk kan namespace-modules automatisch aanmaken — je hebt geen billing.rb nodig tenzij de module methoden of includes nodig heeft.
require_dependency bestaat niet meer
Classic autoloading vertrouwde op require_dependency voor het beheren van laadvolgorde. Zeitwerk gebruikt het niet. Elk require_dependency-aanroep die na een Classic-naar-Zeitwerk-migratie overblijft, is op z’n best dood gewicht en op z’n slechtst gevaarlijk:
# FOUT — doe dit niet met Zeitwerk
require_dependency "billing/invoice"
class OrdersController < ApplicationController
def create
order = Billing::Invoice.create!(order_params)
# ...
end
end
Verwijder elke require_dependency-aanroep. Zoek ze op:
grep -r "require_dependency" app/ lib/
Als het verwijderen iets breekt, is de onderliggende oorzaak een naamgevings- of structuurprobleem — geen laadvolgorde om te patchen. Zeitwerk handelt de laadvolgorde correct af wanneer je bestandsnamen overeenkomen met je constanten.
Eager Loading: Het Productiekloof-Probleem
Het operationeel gevaarlijkste aspect van Rails Zeitwerk autoloading is het standaard configuratieverschil tussen omgevingen:
# config/environments/development.rb
config.eager_load = false # Rails-standaard in development
# config/environments/production.rb
config.eager_load = true # Rails-standaard in productie
Met eager_load = false worden constanten op aanvraag ingeladen. Je testsuite kan elk codepad doorlopen zonder ooit een Zeitwerk-schending te triggeren, omdat de tests toevallig constanten in een volgorde laden waarbij alle namen correct resolven.
Reproduceer het productiegedrag lokaal door dit in een Rails-console uit te voeren:
Rails.application.eager_load!
Elke Zeitwerk::Error die daardoor wordt gegenereerd, is een productie-NameError die wacht om te gebeuren. Los het op voor het live gaat.
De betrouwbaardere plek om dit te vangen is CI. Voeg een aparte stap toe aan je workflow:
# .github/workflows/test.yml
- name: Verifieer Zeitwerk eager loading
run: bin/rails runner "Rails.application.eager_load!"
env:
RAILS_ENV: production
SECRET_KEY_BASE: "ci-placeholder-not-used"
DATABASE_URL: $
Deze stap duurt minder dan vijf seconden en heeft voor mij al meerdere keren een productie-NameError voorkomen. De GitHub Actions CI/CD-gids behandelt de volledige workflow-configuratie als je deze nog niet hebt staan.
Aangepaste Autoload-Paden
Rails voegt standaard app/*-submappen toe aan Zeitwerks autoload-roots. Om je eigen toe te voegen:
# config/application.rb
module MyApp
class Application < Rails::Application
config.autoload_paths << Rails.root.join("lib/my_app")
end
end
Met deze configuratie moet lib/my_app/data_importer.rb de klasse DataImporter definiëren. Een bestand op lib/my_app/importers/csv_importer.rb moet Importers::CsvImporter definiëren.
De meest voorkomende val is lib zelf als autoload-root toevoegen:
# DOE DIT NIET
config.autoload_paths << Rails.root.join("lib")
Met lib als root overschaduwt een bestand genaamd lib/json.rb de standaard Ruby-bibliotheek json gem. Elk bestand in lib wordt potentieel gevaarlijk. Gebruik een naamruimte-submap: lib/my_app/, lib/domain/, lib/services/.
Om te inspecteren wat Zeitwerk tijdens runtime geregistreerd heeft:
# In een Rails-console
Rails.autoloaders.main.dirs # alle autoload-roots
Rails.autoloaders.each(&:log!) # activeer uitgebreide laad-output
Inflections: Zeitwerk Je Afkortingen Leren
Zeitwerk vertaalt bestandsnamen naar constantennamen via ActiveSupports inflector. Gangbare Engelse patronen werken automatisch; domein-afkortingen en onregelmatige namen niet:
# Zonder configuratie:
# app/services/html_parser.rb → Zeitwerk verwacht HtmlParser
# app/models/ttb_client.rb → Zeitwerk verwacht TtbClient
# app/models/oauth_token.rb → Zeitwerk verwacht OauthToken
Stel dit in via de inflections-initializer, die zowel Zeitwerk als ActiveSupport respecteert:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "HTML"
inflect.acronym "TTB"
inflect.acronym "OAuth"
inflect.acronym "API"
inflect.acronym "JSON"
end
Met deze declaraties mapt Zeitwerk html_parser.rb naar HTMLParser, ttb_client.rb naar TTBClient, enzovoort. De inflect.acronym-aanpak verdient de voorkeur boven Zeitwerk-specifieke inflections omdat het consistent doorwerkt in classify, humanize, route-helpers en URL-generatie.
Voor eenmalige uitzonderingen die je niet met een acronymregel kunt oplossen, kun je Zeitwerk direct configureren:
# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"mijn_vreemd_bestandsnaam" => "MijnVreemdConstante"
)
end
Gebruik dit spaarzaam. Meer dan twee of drie van zulke uitzonderingen wijzen gewoonlijk op een naamgevingsprobleem in je conventies.
Migreren van Classic naar Zeitwerk Autoloading
Als je op Rails 6.x zit met config.autoloader = :classic en moet migreren naar Zeitwerk voor een Rails 7+-upgrade, is het proces recht-toe-recht-aan maar methodisch. De Rails upgrade incrementele strategie-post behandelt de bredere upgradeaanpak.
Stap 1: Voer de checker uit en los elke schending op.
bin/rails zeitwerk:check
Hernoem voor elk gerapporteerd bestand het bestand zodat het overeenkomt met de constante (aanbevolen), of hernoem de constante. Doe nu niets anders dan de naamgeving repareren.
Stap 2: Verwijder alle require_dependency-aanroepen.
grep -rn "require_dependency" app/ lib/ | sort
Verwijder elke aanroep. Commit deze verwijderingen apart van de naamgevingsfixes zodat de diff leesbaar blijft.
Stap 3: Voeg eager-loading-verificatie toe aan CI.
Verifieer vóór het overschakelen dat je huidige Classic-codebase de eager-loading-check doorstaat. Dit geeft je een schone uitgangsituatie.
Stap 4: Schakel de autoloader over.
In Rails 6.x-apps doe je dit expliciet:
# config/application.rb
config.autoloader = :zeitwerk
In Rails 7.0+ is Zeitwerk de enige optie — config.load_defaults 7.0 stelt het automatisch in. Er is geen config.autoloader-instelling meer nodig.
Stap 5: Voer zeitwerk:check nogmaals uit en test met eager_load = true.
Na het overschakelen de checker opnieuw uitvoeren — Zeitwerks regels wijken subtiel af van wat Classic tolereerde. Test daarna op staging met config.eager_load = true voordat je naar productie gaat.
De migratie duurt zelden meer dan een paar uur op een goed gestructureerde codebase. Op een codebase met vijftien jaar require_dependency door controllers verspreid en drie verschillende conventies voor domeinlogica, plan je een dag. De RSpec-testpatronen post heeft de testinfrastructuur om regressies te vangen terwijl je de migratie uitvoert.
Zeitwerk-compliance Testen in Je Spec Suite
Voeg een aparte spec toe die de volledige eager loader uitvoert:
# spec/autoloading_spec.rb
require "rails_helper"
RSpec.describe "Zeitwerk autoloader", type: :integration do
it "laadt alle bestanden in zonder fout" do
expect { Rails.application.eager_load! }.not_to raise_error
end
end
Één spec, één bewering, klaar in minder dan een seconde. Zet hem in spec/autoloading_spec.rb in plaats van hem te begraven in een rails helper — hij is belangrijk genoeg om zichtbaar te zijn en moet bij elke CI-build draaien. De meest effectieve bescherming tegen productie-NameErrors die ik ooit aan een Rails-project heb toegevoegd.
Veelgestelde Vragen
Waarom werkt mijn constante in development maar geeft hij een NameError in productie?
Omdat config.eager_load standaard false is in development en true in productie. Met eager loading uit laadt Rails constanten lazy bij eerste gebruik. Als je testsuite of ontwikkelsessie toevallig de juiste laadvolgorde triggert, blijft een verkeerd benoemd bestand onopgemerkt. Eager loading legt de mismatch bloot omdat Zeitwerk elk bestand onvoorwaardelijk bij het opstarten laadt. Reproduceer het lokaal door Rails.application.eager_load! in de development-console uit te voeren voordat je iets oplevvert.
Wat is het verschil tussen autoload_paths en eager_load_paths in Rails?
autoload_paths vertelt Zeitwerk welke mappen hij moet bewaken en lazy vanuit laden in development. eager_load_paths vertelt Rails welke mappen eager geladen moeten worden in productie. In Rails 7+ wordt het toevoegen aan config.autoload_paths automatisch ook in eager_load_paths verwerkt — je hoeft ze niet allebei in te stellen. De enige situatie waarin je eager_load_paths direct zou instellen, is wanneer je een map eager wil laden maar niet beschikbaar wil stellen voor lazy autoloading in development, wat zeldzaam is.
Hoe ga ik om met constanten die dynamisch of binnen blocks gedefinieerd worden?
Zeitwerk kan constanten die binnen blocks, module_eval of via const_set gedefinieerd worden niet autoloaden. Gebruik voor deze gevallen een expliciete require bovenaan het bestand dat de constante nodig heeft. Dynamisch constanten aanmaken is sowieso de moeite waard om te elimineren — het is precies het soort ondoorzichtige laadregel-afhankelijkheid die Classic autoloadings flexibiliteit vroeger maskeerde.
Hoe debug ik een Zeitwerk::Error in productie als ik hem lokaal niet kan reproduceren?
Voer eerst bin/rails zeitwerk:check uit — dit identificeert het probleembestand en de verwachte constante. Vergelijk dat met de werkelijke constante bovenaan dat bestand. Als zeitwerk:check geen schendingen rapporteert, schakel Zeitwerk-logging tijdelijk in: Rails.autoloaders.each(&:log!) in een initializer, deploy naar staging, en bekijk de logs voor welke bestanden worden geladen en in welke volgorde. De log-output is uitgebreid maar de mismatch verschijnt altijd binnen een paar seconden na het opstarten.
Bezig met een Rails-upgrade die vastloopt op Zeitwerk-fouten? TTB Software heeft deze migratie uitgevoerd op codebases uit elk tijdperk van Rails. We weten waar de problemen begraven liggen. Negentien jaar ervaring betekent dat de migratie die er uitziet als een week werk, meestal een dag duurt.
Related Articles
Rails API Versiebeheer: URL Namespaces, Header Routing en Nette Deprecatie
Rails API versiebeheer goed aanpakken: URL namespaces, Accept header routing, controller overerving en Sunset headers...
Solid Queue Recurring Jobs: Vervang Whenever en Sidekiq-Cron in Rails 8
Solid Queue recurring jobs vervangen whenever en sidekiq-cron in Rails 8. Leer recurring task configuratie, dispatche...
Rails Turbo Morphing: Realtime DOM-updates met broadcasts_refreshes
Rails Turbo Morphing patcht de DOM chirurgisch bij een paginavernieuwing. Leer broadcasts_refreshes, scroll-anchoring...