3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

try-catch の書き方で「こうしたらスッキリ書ける!」と気づけた事例がありましたので、今後みなさんがコードを書くときやレビューするときの糧にしていただけたら幸いです。

React Router の loader と ErrorBoundary

本題の前に、この記事が前提とする React Router の仕組みを簡単に整理します。

React Router の v6.4 以降では、ルートごとに loader という関数を定義できます。
これは画面が描画される前にデータを取得しておくための関数で、コンポーネントの useEffect で fetch する代わりに、ルート定義側でデータの準備を済ませられるのが特徴です。

// ルート定義のイメージ
{
  path: "/users/:id",
  loader: userLoader,      // ← 描画前にここでデータ取得
  element: <UserPage />,
  errorElement: <ErrorPage />, // ← エラー時に表示される画面
}

そしてもう一つ、errorElement(ErrorBoundary)という仕組みがあります。
これはルートの処理中に投げられたエラーをキャッチして、エラー画面を表示してくれるものです。この 2 つが、今回の話の土台になります。

今回題材にしたいコード

loadertry-catch を利用して下記のような処理を書いたとします。

export async function loader() {
  try {
    const data = await fetchSomething();
    if (!data) {
      throw new Response("Not Found", { status: 404 });
    }
    return data;
  } catch (error) {
    if (error instanceof Response) {
      throw error; // Response はそのまま投げ直す
    }
    console.error(error);
    throw new Response("Server Error", { status: 500 });
  }
}

通信が失敗するかもしれないし、データが無いかもしれない。
エラーが起きたら困るから、とりあえず try-catch で囲んでおこう。
という気持ちで書くと上記のようなコードになります。

実はこのコードは try-catch を利用することで、むしろコードを読みにくくしていました…。
どう書き換えるとシンプルになるのかを整理していきます。

先に結論

loader の中で「自分でハンドリングすべきエラー」(データなし、バリデーション失敗)だけを明示的に throw し、それ以外のエラーは try-catch せずフレームワークに任せます

何が問題だったのか

冒頭のコードには、3 つの問題が潜んでいます。

問題1: 自分で投げたものを、自分で拾って投げ直している

try ブロックの中で throw したものは、すべて同じ trycatch に入ります。

try {
  throw new Response("Not Found", { status: 404 }); // ← (A) ここで投げると
} catch (error) {
  // ← (B) 必ずここに来る
  if (error instanceof Response) {
    throw error; // ← (C) 同じ Response をもう一回投げ直すだけ
  }
}

(A) で投げた 404 を、(B) の catch が拾って、(C) でまた投げ直しているだけ。
これは 「自分で投げて、自分で拾って、また投げる」という回り道でしかありません。

問題2: そもそも未処理エラーはフレームワークが拾ってくれる

「でも catch しないと、予期しないエラー(fetch の失敗など)が握りつぶされるのでは?」と思うかもしれません。

ここが肝心なところで、React Router は loader 内の未処理エラーを自動的に errorElement(ErrorBoundary)へ渡してくれます

つまり、fetchSomething() が予期せず例外を投げても、try-catch がなくてもエラーはきちんとハンドリングされ、エラー画面に到達します。

catch して 500 に変換する処理は、フレームワークがやってくれることを手作業で再実装していただけなのです…。

問題3: エラー情報がむしろ失われる

catch (error) {
  console.error(error); // 詳細はコンソールにしか出さない
  throw new Response("Server Error", { status: 500 }); // ユーザーには汎用メッセージ
}

元のエラーを console.error に流して、レスポンスには汎用メッセージしか載せていません。
一見「丁寧なエラーハンドリング」に見えます。

React Router は、開発時にはエラーのスタックトレースを表示し、本番では情報漏洩を防ぐため自動でサニタイズします。
自前で 500 エラーを返してしまうと、この開発時の便利なデバッグ表示を潰してしまうのです。

書き換え後

try-catch を丸ごと外して、こうしました。

import { data } from "react-router";

export async function loader() {
  const result = await fetchSomething();

  // 自分でハンドリングすべきものだけ明示的にチェック
  const raw = Array.isArray(result) ? result[0] : result;
  if (!raw) {
    throw new Response("Not Found", { status: 404 });
  }

  const parsed = SomeSchema.safeParse(raw);
  if (!parsed.success) {
    throw data(null, { status: 404 });
  }

  return parsed.data;
}

ネストが消えてフラットになり、「何をハンドリングしたいのか」が一目で分かるようになりました。

data() は React Router が提供するヘルパーで、ステータスコード付きのレスポンスを返したいときに使えます。
throw data(null, { status: 404 }) のように投げると、ErrorBoundary 側で isRouteErrorResponse を使って拾えます。

なぜこれで十分なのか

ケースごとに、Before / After の挙動を並べてみます。

ケース Before(try-catch あり) After(try-catch なし)
データなし try 内で 404 → catch で再スロー 直接 404 を throw
バリデーション失敗 try 内で 500 → catch で再スロー 直接 404 を throw
接続エラー catch で 500 に変換 React Router が自動処理
予期しないエラー catch で 500 に変換 React Router が自動処理

どのケースも、最終的な挙動は変わりません。
フレームワークに任せるだけでコードは短く、デバッグ情報も保たれます。

学び: 「フレームワークが守ってくれる範囲」を知る

今回の本質は、try-catch そのものが悪いわけではないということです。

問題は、フレームワークが既にやってくれることを知らずに、手作業で再実装してしまっていたこと。
React Router の「loader の未処理エラーは ErrorBoundary に流れる」という仕様を理解していれば、catch が不要だと判断できます。

エラーハンドリングを書く前に

  • このエラーは「自分が意図的にハンドリングすべきもの」か
  • それとも「フレームワークに任せたほうがいいもの」か

を問い直せると良いでしょう。

意図的にハンドリングすべきエラー(データなし、バリデーション失敗)だけを明示的にチェックし、それ以外はフレームワークに委ねる。
これが、loader をシンプルに保つコツでした。

まとめ

  • React Router の loader 内の未処理エラーは、自動で ErrorBoundary に渡される
  • 自前で 500 エラーに変換すると、開発時のスタックトレース表示まで潰してしまう
  • 「自分でハンドリングすべきエラーだけ明示し、あとは任せる」とコードがフラットになる

AI に任せることが多くなった昨今ですが、良いコードの判断は自分でできるようにしておきたいですね!
まだまだ精進します!

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?