Rails「ActiveRecord::RecordNotFound」エラーの原因と対処法|404ハンドリングと例外設計

Rails「ActiveRecord::RecordNotFound」エラーの原因と対処法|404ハンドリングと例外設計

Ruby on Rails 開発で頻発するエラー:

ActiveRecord::RecordNotFound (Couldn't find User with 'id'=999)
ActiveRecord::RecordNotFound (Couldn't find Post with [WHERE "posts"."slug" = $1])
ActiveRecord::RecordNotFound: Couldn't find all Articles with 'id': (1, 2, 3) (found 2 results, but was looking for 3)

ActiveRecord::RecordNotFound は「指定したレコードが見つからない」場合に発生する例外。find メソッドの仕様通りの動作ですが、

  • 開発中に頻発してログが煩い
  • API で 500 エラーを返してしまう
  • 「404 Not Found」を返したいのに 500 になる
  • カスタムエラーページが表示されない
  • N+1で大量に出ることがある
  • find_by! と find はどちらを使うべき?

など、設計判断に迷うポイントが多くあります。

本記事では、ActiveRecord::RecordNotFoundすべての原因と対処法を、現場で即使えるパターンとして整理します。エラーの本質、find系メソッドの違い、rescue_from による404ハンドリング、API・JSONレスポンスでのカスタマイズ、関連エラー、デバッグ手法、FAQまで完全網羅。この1本でRails 404 ハンドリングをマスターできます。


目次

結論:今すぐ試すべき3ステップ

時間がない方向けに、まず実施すべき手順を示します。

ステップ1:発生原因を特定

ActiveRecord::RecordNotFound (Couldn't find User with 'id'=999)
                                       ↑          ↑
                                    モデル      検索条件

スタックトレースから発生位置確認:

app/controllers/users_controller.rb:8:in `show'

ステップ2:意図通りなら 404 として処理

find でレコードが見つからないのは自然。Rails では標準で 404 を返す:

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    # 見つからなければ自動的に 404 ページ
  end
end

開発環境では例外画面が出ますが、本番では public/404.html が表示。

ステップ3:カスタマイズしたい場合

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound do |exception|
    render json: { error: exception.message }, status: :not_found
    # または
    # render template: "errors/not_found", status: :not_found
  end
end

詳細は以下で解説します。


まず押さえる:エラーの本質

ActiveRecord::RecordNotFound とは

ActiveRecordfind 系メソッドが該当レコードを発見できなかった時に発生する例外です。

User.find(999)
# => ActiveRecord::RecordNotFound (Couldn't find User with 'id'=999)

Rails の自動404変換

ApplicationController のデフォルト動作として、Rails は以下の例外を自動的にHTTPステータスコードにマッピングします:

# rails/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
ActionDispatch::ExceptionWrapper.rescue_responses
# => {
#   "ActiveRecord::RecordNotFound" => :not_found,
#   "ActionController::RoutingError" => :not_found,
#   "ActionController::UnknownController" => :not_found,
#   "ActionController::BadRequest" => :bad_request,
#   "ActionController::ParameterMissing" => :bad_request,
#   ...
# }

つまり、ActiveRecord::RecordNotFound本番環境では自動的に404になります。開発環境では例外画面が出るだけ。

開発環境 vs 本番環境

環境RecordNotFound の挙動
developmentエラー画面(スタックトレース表示)
testテストが失敗(例外を rescue しない場合)
production自動的に 404 ページを表示

config/environments/production.rb の以下の設定で制御:

config.consider_all_requests_local = false  # false なら本番モードで404に

なぜエラーで困るのか

問題なケース:

  • 意図しない場所で発生: 検索処理途中など
  • APIで500を返す: JSON APIで404を返すべき場面
  • ログが大量: ボット等が無効URLを叩くと記録される
  • UX悪化: デフォルト404ページが汎用的すぎる

find系メソッドの完全比較

ActiveRecord::RecordNotFound を理解するには、find 系の挙動を把握することが重要。

find(最頻出)

User.find(1)
# => レコードあれば返す
# => 無ければ RecordNotFound 例外

User.find(1, 2, 3)
# => 3件全部見つかればArray
# => 1件でも見つからなければ RecordNotFound

find_by(nil返却)

User.find_by(email: "alice@example.com")
# => レコードあれば返す
# => 無ければ nil

find_by!(例外)

User.find_by!(email: "alice@example.com")
# => レコードあれば返す
# => 無ければ RecordNotFound

first / last

User.first   # => 最初のレコード or nil
User.last    # => 最後のレコード or nil

first! / last!(例外)

User.first!  # => レコードあれば返す、無ければ RecordNotFound
User.last!   # => 同上

where().first

User.where(active: true).first
# => 条件マッチの最初 or nil

User.where(active: true).first!
# => マッチなしなら RecordNotFound

take / take!

User.take    # => 任意の1件 or nil
User.take!   # => 任意の1件 or RecordNotFound(無ければ)

sole / find_sole_by(Rails 7+)

User.where(email: "alice@example.com").sole
# => 厳密に1件だけ
# => 0件: RecordNotFound
# => 2件以上: ActiveRecord::SoleRecordExceeded

完全比較表

メソッド引数見つかった見つからない
find(id)id(PK)レコードRecordNotFound
find(ids)配列レコード配列RecordNotFound(1件でも不足)
find_by(...)条件レコードnil
find_by!(...)条件レコードRecordNotFound
first / lastなしレコードnil
first! / last!なしレコードRecordNotFound
takeなしレコードnil
take!なしレコードRecordNotFound
soleなしレコード(厳密1件)RecordNotFound / SoleRecordExceeded

使い分け指針

# 1. URLパラメータから検索 → find(404が自然)
def show
  @user = User.find(params[:id])
end

# 2. 任意条件検索(不在も正常) → find_by
def signup
  if User.find_by(email: params[:email])
    redirect_to login_path, alert: "既に登録済み"
  end
end

# 3. 必須レコードの取得 → find_by!
def admin
  @admin = User.find_by!(role: "admin")
end

# 4. 最初の1件取得 → first / first!
@latest = Article.order(created_at: :desc).first

発生パターン別の対処

パターン1:URLからの不正なID

def show
  @user = User.find(params[:id])   # 999 みたいな存在しないID
end

対処: 自動 404 で問題なし。カスタマイズしたい場合は rescue_from

パターン2:APIでJSON形式404を返したい

class Api::UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do
    render json: { error: "User not found" }, status: :not_found
  end
  
  def show
    @user = User.find(params[:id])
    render json: @user
  end
end

パターン3:複数の find チェーン

def show
  @user = current_user.posts.find(params[:id])
  # ・current_user が nil → NoMethodError
  # ・posts が空 → 問題なし
  # ・id が存在しない → RecordNotFound
end

current_user.posts.find は「現在のユーザーの投稿の中から探す」ため、他人の投稿IDを指定すると404になる適切な動作です。

パターン4:スコープでの絞り込み後

def show
  @article = Article.published.find(params[:id])
  # 公開済み記事の中で探す
  # 下書きや非公開の場合は RecordNotFound
end

セキュリティ的にも有用(公開記事のみ閲覧可能を強制)。

パターン5:複数条件で find_by!

def edit
  @article = current_user.articles.find_by!(slug: params[:slug])
  # slug で検索、なければ 404
end

パターン6:複数IDの一括検索

def show_multiple
  @articles = Article.find([1, 2, 3])
  # 3件全部見つからなければ RecordNotFound
end

許容したい場合:

@articles = Article.where(id: [1, 2, 3])
# 見つかった分だけ返す(空配列もOK)

rescue_from による404ハンドリング

カスタマイズの基本パターン。

全コントローラで共通の404処理

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  
  private
  
  def record_not_found(exception)
    render template: "errors/not_found", status: :not_found
  end
end

ブロック形式

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound do |exception|
    Rails.logger.warn "Not Found: #{exception.message}"
    render template: "errors/not_found", status: :not_found
  end
end

コントローラごとの個別処理

class ArticlesController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do
    redirect_to articles_path, alert: "記事が見つかりません"
  end
  
  def show
    @article = Article.find(params[:id])
  end
end

複数の例外をまとめて処理

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound,
              ActionController::RoutingError,
              ActionController::ParameterMissing,
              with: :handle_404
  
  rescue_from StandardError, with: :handle_500 unless Rails.env.development?
  
  private
  
  def handle_404
    respond_to do |format|
      format.html { render template: "errors/not_found", status: :not_found }
      format.json { render json: { error: "Not Found" }, status: :not_found }
      format.any { head :not_found }
    end
  end
  
  def handle_500(exception)
    Rails.logger.error "Internal Error: #{exception.message}"
    Rails.logger.error exception.backtrace.join("\n")
    
    respond_to do |format|
      format.html { render template: "errors/internal_server_error", status: :internal_server_error }
      format.json { render json: { error: "Internal Server Error" }, status: :internal_server_error }
    end
  end
end

rescue_from の順序

# 上から順にチェックされる
rescue_from StandardError, with: :handle_500       # ← 最後の砦
rescue_from ActiveRecord::RecordNotFound, with: :handle_404

より具体的な例外を先に書くのが原則。StandardError を先に書くと他の rescue_from が動きません。


ローカルコントローラ内での rescue

特定アクションだけ別の挙動にしたい場合:

class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    redirect_to articles_path, alert: "記事が見つかりません"
  end
end

begin / rescue ブロック

def show
  begin
    @article = Article.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    Rails.logger.warn "RecordNotFound: #{e.message}"
    redirect_to articles_path
  end
end

APIモードでのカスタマイズ

JSON API では HTML 404 ページではなく JSON レスポンスを返したい。

基本パターン

class Api::ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
  rescue_from ActionController::ParameterMissing, with: :parameter_missing
  
  private
  
  def record_not_found(exception)
    render json: {
      error: {
        type: "RecordNotFound",
        message: exception.message
      }
    }, status: :not_found
  end
  
  def record_invalid(exception)
    render json: {
      error: {
        type: "RecordInvalid",
        message: exception.message,
        errors: exception.record.errors.full_messages
      }
    }, status: :unprocessable_entity
  end
  
  def parameter_missing(exception)
    render json: {
      error: {
        type: "ParameterMissing",
        message: exception.message,
        parameter: exception.param
      }
    }, status: :bad_request
  end
end

JSON:API 仕様準拠

def record_not_found(exception)
  render json: {
    errors: [
      {
        status: "404",
        title: "Not Found",
        detail: exception.message
      }
    ]
  }, status: :not_found
end

エラー詳細の制御

開発環境では詳細表示、本番では汎用メッセージ:

def record_not_found(exception)
  message = if Rails.env.development?
    exception.message
  else
    "リクエストされたリソースが見つかりません"
  end
  
  render json: { error: message }, status: :not_found
end

404ページのカスタマイズ

Rails 標準の404ページ

public/404.html

このファイルを編集すれば、デフォルトの404ページが変わります。

Erbテンプレートとして処理

# config/application.rb
config.exceptions_app = self.routes
# config/routes.rb
match '/404', to: 'errors#not_found', via: :all
match '/422', to: 'errors#unprocessable_entity', via: :all
match '/500', to: 'errors#internal_server_error', via: :all
# app/controllers/errors_controller.rb
class ErrorsController < ApplicationController
  layout "error"
  
  def not_found
    render status: :not_found
  end
  
  def unprocessable_entity
    render status: :unprocessable_entity
  end
  
  def internal_server_error
    render status: :internal_server_error
  end
end
<%# app/views/errors/not_found.html.erb %>
<div class="error-page">
  <h1>404 - ページが見つかりません</h1>
  <p>お探しのページは削除されたか、URLが間違っている可能性があります。</p>
  <%= link_to "ホームに戻る", root_path, class: "btn" %>
  
  <%= form_tag search_path, method: :get do %>
    <%= text_field_tag :q, params[:q], placeholder: "検索" %>
  <% end %>
</div>

動的ページにする利点

  • ヘッダー・フッター共通化
  • アナリティクス計測タグ埋め込み
  • 検索ボックスや関連リンク表示
  • 国際化(i18n)対応

関連メソッドの活用

find_or_initialize_by

「見つかれば返す、無ければ新規オブジェクト(DB保存はしない)」:

user = User.find_or_initialize_by(email: "alice@example.com")

if user.new_record?
  user.name = "Alice"
  user.save
end

find_or_create_by

「見つかれば返す、無ければ DB保存して返す」:

user = User.find_or_create_by(email: "alice@example.com") do |u|
  u.name = "Alice"
end

⚠️ 競合状態の罠: 並行リクエストで同じレコード重複作成の可能性。find_or_create_by! で検証エラーを例外化、または unique index で DB レベル制約推奨。

find_each(バッチ処理)

大量レコード処理時:

User.find_each(batch_size: 100) do |user|
  user.update_score
end

メモリ効率を保ちつつ全件処理。途中で RecordNotFound は出ない(あれば内部的に空配列扱い)。


DB index と RecordNotFound パフォーマンス

find(id) は主キー検索なので極めて高速find_by(条件) は条件カラムにインデックスがないと遅くなります。

# 主キー検索(O(log n))
User.find(1)
# SELECT * FROM users WHERE id = 1 LIMIT 1

# 非インデックス列での検索(O(n))
User.find_by(email: "...")   # email にindex無いと遅い

インデックス追加

class AddIndexToUsersEmail < ActiveRecord::Migration[8.0]
  def change
    add_index :users, :email, unique: true
  end
end

unique: true で重複保存も防げます。


ログの読み方とデバッグ

スタックトレース

ActiveRecord::RecordNotFound (Couldn't find User with 'id'=999):
  
app/controllers/users_controller.rb:8:in `show'
app/controllers/application_controller.rb:5:in `block in <main>'

ポイント:

  • エラー直後: Couldn't find User with 'id'=999 から条件と何が無いか分かる
  • 最上位の自分のコード: app/controllers/users_controller.rb:8が該当行

ログレベル設定

本番では大量に出ることがあるので、ログレベル調整:

# config/application.rb
config.action_dispatch.rescue_responses["ActiveRecord::RecordNotFound"] = :not_found

# 404 のログを WARN に抑える
config.log_level = :info

または rescue_from 内で:

rescue_from ActiveRecord::RecordNotFound do |exception|
  Rails.logger.warn "404: #{exception.message} [#{request.method} #{request.fullpath}]"
  render template: "errors/not_found", status: :not_found
end

Sentry / Rollbar 等での監視

通常、ボットや古いブックマークによる404はノイズ。エラートラッカーで除外設定:

# config/initializers/sentry.rb
Sentry.init do |config|
  config.excluded_exceptions += [
    'ActiveRecord::RecordNotFound',
    'ActionController::RoutingError'
  ]
end

ベストプラクティス

1. find vs find_by の使い分け

シーン推奨
URLからのID(不在=404)find
任意条件検索(不在=正常)find_by
必須レコード(不在=異常)find_by!

2. ApplicationController に共通 rescue_from

すべてのコントローラに重複実装しない。

3. API は JSON 形式で返却

HTML 404 ページを返さない設計に。

4. セキュリティ的活用

# ❌ 他人の投稿も編集可能
@post = Post.find(params[:id])

# ✅ 自分の投稿のみ
@post = current_user.posts.find(params[:id])
# 他人の投稿IDなら 404(権限エラーではなく存在隠蔽)

5. リダイレクト先の検討

rescue_from ActiveRecord::RecordNotFound do
  redirect_to root_path, alert: "ページが見つかりません"
  # または
  render "errors/not_found", status: :not_found
end

UXによって使い分け。

6. テストでカバー

RSpec.describe "GET /users/:id" do
  it "存在しないIDで404" do
    get "/users/999"
    expect(response).to have_http_status(:not_found)
  end
end

7. find_by の戻り値は必ずチェック

# ❌ 危険
user = User.find_by(email: email)
user.send_email   # nil の可能性

# ✅ 安全
user = User.find_by(email: email)
return unless user
user.send_email

find_by 後の nil 確認はundefined method nil:NilClassの記事を参照。


トラブルシューティング

404になるべきが500になる

rescue_from StandardError が先に書かれている可能性:

# ❌ 順序が悪い
rescue_from StandardError, with: :handle_500  # ← これが先に発火
rescue_from ActiveRecord::RecordNotFound, with: :handle_404

# ✅ 具体的な例外を先に
rescue_from ActiveRecord::RecordNotFound, with: :handle_404
rescue_from StandardError, with: :handle_500

本番でカスタム404ページが出ない

# config/environments/production.rb で
config.consider_all_requests_local = false   # ← false にする
config.exceptions_app = self.routes           # ← 動的404の場合

開発環境で404画面を見たい

# config/environments/development.rb
config.consider_all_requests_local = false

または、URLに ?debug=false を付けて試す。

ActiveRecord::RecordNotFound: Couldn’t find all

ActiveRecord::RecordNotFound: Couldn't find all Articles with 'id': (1, 2, 3) (found 2 results, but was looking for 3)

User.find([1, 2, 3]) で1件でも見つからない時に発生。

対処:

Article.where(id: [1, 2, 3])
# 見つかった分だけ返す

ActiveRecord::SoleRecordExceeded

User.where(email: "common@example.com").sole
# 2件以上ヒット → SoleRecordExceeded

sole は厳密に1件期待する場合のみ使用。

ボットからの404大量発生

GET /wp-admin
GET /.env
GET /admin/login

セキュリティスキャンによる404の嵐。対処:

  1. ログレベルを下げる
  2. Rate Limiting(rack-attack 等)
  3. WAF / CDN レベルでブロック

関連エラーとの違い

NilClass NoMethodError

@user.name
# @user が nil なら NoMethodError

find_by の戻り値(nil)に対してメソッドを呼ぶと出ます。詳細はundefined method nil:NilClass の記事

ActionController::RoutingError

ActionController::RoutingError (No route matches [GET] "/nonexistent")

URLにマッチするルートがない場合。これも404ですが、RecordNotFoundとは別物。

ActiveRecord::RecordInvalid

User.create!(email: "invalid")
# => ActiveRecord::RecordInvalid (Validation failed: ...)

バリデーション失敗時。

ActiveRecord::StatementInvalid

ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column does not exist

SQL自体が無効。マイグレーション漏れ等。

ActionController::ParameterMissing

params.require(:user).permit(:name)
# params[:user] が無い場合

違いの一覧

例外発生原因HTTPステータス(自動)
ActiveRecord::RecordNotFoundfind/find_by!でレコード不在404
ActionController::RoutingErrorルート未定義404
ActiveRecord::RecordInvalidバリデーション失敗422
ActiveRecord::StatementInvalidSQL実行失敗500
ActionController::ParameterMissingパラメータ不足400

よくある質問(FAQ)

Q1. find と find_by はどちらを使うべきですか?

シーン推奨
URLパラメータからの取得find(不在=404)
任意条件・不在も正常find_by(nil返却)
必須レコードfind_by!(不在=例外)

Q2. 404 にカスタム HTML を返したい

# config/routes.rb
match '/404', to: 'errors#not_found', via: :all

# config/application.rb
config.exceptions_app = self.routes

app/controllers/errors_controller.rbnot_found アクション定義。

Q3. API でカスタム JSON を返したい

class Api::ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound do |e|
    render json: { error: e.message }, status: :not_found
  end
end

Q4. テストで RecordNotFound を確認する方法

# RSpec
it "存在しないIDで404" do
  get "/users/999"
  expect(response).to have_http_status(:not_found)
end

# Minitest
test "404 for non-existent user" do
  get "/users/999"
  assert_response :not_found
end

Q5. RecordNotFound を意図的に発生させる方法

raise ActiveRecord::RecordNotFound, "Custom message"

特定条件でメソッドの途中から 404 を返したい場合に使えます。

Q6. 404 のログを抑制したい

rescue_from ActiveRecord::RecordNotFound do |e|
  Rails.logger.warn(e.message)  # WARN レベルで記録
  render "errors/not_found", status: :not_found
end

Sentry等のエラートラッカーでも除外推奨。

Q7. find のN+1問題はありますか?

# ❌ N+1
@articles = Article.find([1, 2, 3])
@articles.each { |a| a.author.name }   # author の SELECT が3回

# ✅ includes で解決
@articles = Article.includes(:author).find([1, 2, 3])

Q8. routes でidが文字列だと?

get '/articles/:id', to: 'articles#show'
Article.find("abc")
# => ActiveRecord::RecordNotFound (Couldn't find Article with 'id'=abc)

数字以外も find に渡されますが、PostgreSQL等では型変換エラーになる場合も。

Q9. 404 でリダイレクトすべき?

UXによります:

  • エラーページ表示: 「404 – 見つかりません」(検索ヒント付き)
  • トップにリダイレクト: redirect_to root_path, alert: "..."
  • API: JSON エラーレスポンス

ユーザーの混乱を最小化する方法を選択。

Q10. find の前にバリデーションすべき?

整数チェック等は事前にした方が良い:

def show
  unless params[:id].to_s.match?(/\A\d+\z/)
    return render status: :bad_request
  end
  
  @user = User.find(params[:id])
end

ただし、Rails は通常型変換を自動でしてくれます。

Q11. SoftDelete を使っている場合

paranoiadiscard gem で論理削除している場合:

# 論理削除されたレコードは find しても見つからない
User.find(deleted_user_id)
# => RecordNotFound

# 削除済みも含めて検索
User.with_deleted.find(deleted_user_id)

Q12. UUID をPKに使う場合

class User < ApplicationRecord
  # id がUUIDの場合
end

User.find("invalid-uuid")
# => ActiveRecord::StatementInvalid(型変換エラー)
# RecordNotFound にしたい場合は事前バリデーション
def show
  unless params[:id].match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
    raise ActiveRecord::RecordNotFound
  end
  
  @user = User.find(params[:id])
end

参考リンク・関連資料

Rails 公式

関連 Gem

関連記事(本サイト)


まとめ

ActiveRecord::RecordNotFound は Rails で頻出ですが、Rails の標準的な404処理として活用することで、シンプルかつ堅牢なコードを書けます。要点を再整理します。

  • 本質: find系メソッドの「レコード未発見」例外、本番では自動的に404
  • find系の使い分け: find(404)、find_by(nil)、find_by!(例外)、first/take系
  • rescue_from: ApplicationController で共通404処理
  • API では: JSON フォーマットでステータス404を返す
  • 404ページのカスタマイズ: routes + ErrorsController で動的化
  • セキュリティ的活用: current_user.posts.find で権限エラーを存在隠蔽
  • 関連メソッド: find_or_initialize_by、find_or_create_by、find_each
  • ベストプラクティス: 具体的例外を先に rescue、テストでカバー、ログ抑制

これらの知識は、Rails の Web アプリ・API 開発・本番運用すべてで活用できます。本記事をブックマークしておけば、404 ハンドリングと例外設計を適切に実装できるようになります。


本記事は2026年6月時点の情報をもとに、Ruby on Rails 7.x / 8.x での動作確認・公式ドキュメントに基づき作成しています。Rails のバージョンによって挙動が異なる場合があるため、最新の情報はRails公式ガイドもあわせてご確認ください。