0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails autoload_paths × zeitwerk × bootsnapの基礎とFrozenErrorの読み解き

0
Last updated at Posted at 2026-05-09

背景

Rails 8 アプリで bundle exec rspec を実行したら、下記のエラーが出た

an error occurred while loading ./spec/jobs/sample_import_job_spec.rb.
Failure/Error: require_relative '../config/environment'

FrozenError:
  can't modify frozen Array: ["/Users/user/.local/share/ .....

エラー文に turbo-railsactiontext が並ぶので一見これらの gem が悪そうに見えるが、実は犯人ではない

本質は Rails の自動読み込みを支える 3 つの概念 — autoload_paths / zeitwerk / bootsnap — の連携と「凍結」のタイミングにある

結論から言うと、tmp/cache/bootsnap のキャッシュを削除したら直った。

本記事ではなぜそれで直るのかまで踏み込む

エラーの正体: FrozenError とは

FrozenError:
  can't modify frozen Array: [".../turbo-rails/app/channels", ...]

FrozenError は Ruby の組み込みエラーで、freeze メソッドで凍結された(変更不可になった)オブジェクトを書き換えようとしたときに発生する

今回凍結されてた配列の正体は ActiveSupport::Dependencies.autoload_paths で、自動読み込み対象フォルダの一覧データだ。エラーメッセージに表示される turbo-railsactiontext のパスは、その時点で配列の中身に入ってたフォルダを表示してるだけで、これらの gem が原因というわけではない

登場人物 3つの関係

Rails の自動読み込みには 3 つの主要概念が登場する。役割を表で整理する

概念 役割 比喩
autoload_paths 自動読み込み対象フォルダのリスト(データ) 住所録
zeitwerk リストを使って実際にファイルを読み込む仕組み(プログラム) 司書
bootsnap 起動を高速化するためのキャッシュ層 メモ帳係

3 つが揃って初めて Rails の自動読み込みが機能する

autoload_paths(住所録)

「Rails が自動でクラスを探しに行くフォルダ群を並べた配列」。Rails の住所録だと捉えると分かりやすい

autoload_paths = [
  "app/models",
  "app/controllers",
  "app/jobs",
  ...(gem たちが追加したフォルダも含む)
]

各 gem(Engine)は自分の app/ 配下フォルダを「うちも読み込み対象に入れて」と autoload_paths に unshift で追加する

Ruby では本来、他のファイルのコードを使うには require で明示的に読み込む必要があるが、Rails では User と書くだけで自動的に app/models/user.rb を探して読み込んでくれる。これを支えてるのがこの仕組み

zeitwerk(司書)

autoload_paths は「どこを巡回するか」のリストでしかない。実際にフォルダをスキャンしてファイルを読み込み、User という定数が呼ばれたら app/models/user.rb を読み込んで応答する役割を担うのが zeitwerk

[autoload_paths]              [zeitwerk]
"巡回すべき本棚一覧"          "司書(実際に動く人)"

  ┌──────────┐
  │ A 棚      │
  │ B 棚      │ ──── 渡す ───→ 「了解、巡回します」
  │ C 棚      │
  └──────────┘

歴史的経緯として、Rails 6 までは classic という古いオートローダーが使われていたが、Rails 7 以降は zeitwerk がデフォルトになった。Rails 8 + zeitwerk 2.7 系では autoload_paths を凍らせるタイミングが早くなり、後から .concat する gem が弾かれるようになった点が今回のエラーの遠因にもなっている

bootsnap(メモ帳係)

Rails の起動を高速化するための gem。素の Rails 起動は

  • 何百もの Ruby ファイルを読み込み・パース
  • 何十もの YAML 設定ファイルを解釈
  • 「どのフォルダに何のファイルがあるか」を全フォルダ走査

を毎回ゼロからやるため 5〜30 秒かかる。bootsnap はこれらの結果を tmp/cache/bootsnap/ にキャッシュして再利用する

キャッシュ 内容 効果
load-path-cache どのフォルダにどの Ruby ファイルがあるかの場所メモ フォルダ総当たり捜索を回避
compile-cache-iseq Ruby コードを機械が分かる形(バイトコード)に翻訳した結果 翻訳作業を回避
compile-cache-yaml YAML を Ruby が扱える形に解釈した結果 解釈作業を回避

キャッシュは速いが「実物が変わったらキャッシュも更新する必要がある」というジレンマを抱える。bootsnap は基本ファイル更新日時で判定して自動更新するが、Rails のメジャーアップデート時、gem 入れ替え時、Ruby のバージョン変更時、bundle install を中断した時など、稀にミスる

Rails 起動シーケンスと freeze のタイミング

Rails アプリの起動は「準備フェーズ」と「稼働フェーズ」に分かれており、autoload_paths は準備フェーズの最後に freeze される

起動 ─→ [準備] gem が住所録に追加 ─→ freeze ─→ [稼働] アプリ運転
                                       ↑
                            「住所録は完成、もう触るな」

freeze する理由は 3 つある

  • 安全: 稼働中に住所録が書き換わると「さっきまで見えてたフォルダが消えた」事故が起きる
  • 高速化: もう変わらないと確定すれば、Rails が先回り最適化できる
  • バグ防止: 開発者がうっかり実行中に書き換えるのを禁じる

freeze している実コードは railtiesfinisher.rb にある

# railties-8.0.2.1/lib/rails/application/finisher.rb:24
ActiveSupport::Dependencies.autoload_paths.freeze

Finisher(仕上げ係)という名前のとおり、起動準備の最後の方で凍らせている

正常な起動の流れは以下のとおり

  1. 集計: 各 gem が autoload_paths に追加
  2. 凍結: Rails が autoload_paths を freeze
  3. 登録: Rails が autoload_paths を zeitwerk に push_dir で渡す
  4. スキャン: zeitwerk がフォルダ内をスキャンし、「app/models/user.rbUser 定数」の対応表を作成
  5. 稼働: プログラム中で User が呼ばれたら、zeitwerk が user.rb を読み込んで定数を提供

なぜ FrozenError が起きるか

.concat は Ruby の機能で「配列の後ろに別の配列をくっつける」操作

list = ["りんご", "みかん"]
list.concat(["バナナ"])
# => ["りんご", "みかん", "バナナ"]

今回のエラーは、誰かが住所録に対してこの .concat をしようとしたが、住所録はすでに freeze 済みだったため Ruby が「書き込み禁止」と弾いた、という構図

誰か:「住所録にフォルダ足すで」.concat
Ruby:「アカン、もう凍ってる!」
結果:FrozenError 💥

正常な順番と壊れた順番を比較するとこうなる

[正常な順番]
1. gem たちが住所録に .concat で追加
2. Rails が freeze
3. アプリ稼働

[壊れている順番]
1. gem たちが追加
2. Rails が freeze
3. 誰かが「あ、追加忘れてた」.concat ← 💥

そしてこの順番ズレを引き起こす一番ありがちな原因が、bootsnap の古いキャッシュ

[古いキャッシュを信じた起動]
1. bootsnap「昔のメモやと A → B → C の順」
2. でも今の Rails は B → A → C の順で動くべき
3. 結果:「freeze」と「.concat」のタイミングが入れ替わる
4. → FrozenError 💥

キャッシュを消せば、最新の状態でメモを作り直す → 順番ズレ解消、という流れ

解決アプローチ 2案

やること なぜ効くか おすすめ度
1. bootsnap キャッシュクリア rm -rf tmp/cache/bootsnap 古いメモを捨てて最新状態で作り直させる。順番ズレを解消 ★★★ まずこれ
2. フルバックトレース取得 bundle exec rspec --backtrace ... RSpec の config.filter_rails_from_backtrace! で隠れていた gem 内部のフレームを表示 → 真犯人を特定する診断手段 ★★ 案1で直らない時

bootsnap キャッシュ削除は安全

rm -rf tmp/cache/bootsnap
  • 本体コードへの影響なし
  • 次回起動時に bootsnap が自動でキャッシュを作り直す
  • 副作用は「次の 1 回だけ起動が 10〜30 秒遅い」だけ
  • 2 回目以降は元の速度に戻る

「困ったらまずキャッシュ消し」は Rails 開発の鉄板で、原因不明の起動エラーの 3〜4 割はこれで直る

RSpec のバックトレース隠蔽の落とし穴

spec/rails_helper.rb 内の以下の設定により、Rails / gem 内部のフレームが標準では非表示になる

config.filter_rails_from_backtrace!

普段はノイズ削減で便利だが、犯人が gem 内部にいる時は真因が隠れる。対処は次のいずれか

  • --backtrace オプションを付けて実行
  • もしくは一時的に上記設定をコメントアウト

よくある誤解

  • ❌「turbo-railsactiontext が悪さしてる」 → 正: エラーメッセージの配列の中身に表示されるだけ。犯人は別
  • ❌「view_component が原因」 → 正: test/components/previews/ を持たないプロジェクトでは view_component-4.8.0/lib/view_component/engine.rb:41.concat は早期 return でスキップされる
  • ❌「メモ(bootsnap キャッシュ)を消すと壊れる」 → 正: 本体コードに影響なし、次回起動で自動再生成

まとめ

FrozenError: can't modify frozen Array を見たら

  1. まず rm -rf tmp/cache/bootsnap を試す(9 割これで直る)
  2. 直らなければ --backtrace で原因 gem を特定

3 つの概念の関係

  • autoload_paths: 自動読み込み対象フォルダのリスト(データ・住所録)
  • zeitwerk: リストを使って実際にファイルを読む仕組み(プログラム・司書)
  • bootsnap: 起動を高速化するためのキャッシュ層(メモ帳係)

Rails 起動シーケンス

各 gem が autoload_paths に .concat で追加
            ↓
Rails が autoload_paths を freeze(finisher.rb:24)
            ↓
zeitwerk に push_dir で登録(finisher.rb:30 周辺)
            ↓
アプリ稼働開始

感想

  • bundle exec rspec で出たエラーは表面だけ見ると turbo-railsactiontext を疑いたくなるが、実態は bootsnap が抱えてた古いキャッシュ

  • 「キャッシュ削除で直る」だけじゃなくて原因まで知れたのが収穫だった

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?