メインコンテンツまでスキップ

第118章:エラーの処理 (1)


この章のテーマはズバリ、

use(Promise) でデータ取ってるとき、エラーが出たらどうするの?🤯 ふつうの JavaScript みたいに try-catch じゃダメなの?」

っていう話です。

React v19 からの「useSuspense スタイルのデータ取得」では、 エラーの扱い方の考え方がちょっとだけ特殊なので、ここで一度整理します 🧠✨

次の章(119章)で本命の「エラーバウンダリ」をガッツリやるので、 ここでは 「なぜ try-catch がダメっぽいのか」「代わりにどう考えるのか」 をつかむのがゴールです。


1️⃣ まず復習:use(Promise) の世界で何が起こってる?

React 19 の use フックをおさらいします 👀

  • const data = use(somePromise);
  • somePromisepending → コンポーネントは「Promise を throw」する → いちばん近い <Suspense> がそれをキャッチして fallback(ローディング画面)を表示する
  • somePromiseresolvedata に結果が入り、ふつうにレンダー
  • somePromisereject → コンポーネントは「Error を throw」する → いちばん近い エラーバウンダリ がキャッチして「エラー用 UI」を表示する (Medium)

ポイントは、

「Promise の状態に応じて、コンポーネントが throw してる」

ってことです。 これが Suspense(待ち)エラーバウンダリ(失敗) につながります。


2️⃣ try-catch で囲めばよくない? → 実はうまくいかない 🙅‍♀️

JavaScript だけの世界なら、よくこんな書き方しますよね 👇

try {
const res = await fetch("/api/data");
const data = await res.json();
} catch (error) {
console.error("エラーだよ", error);
}

じゃあ React コンポーネントで:

// ❌ やりがちな「ダメな例」
import { use } from "react";
import { fetchUsers } from "./api";

export function BadUserList() {
try {
const users = use(fetchUsers()); // ← ここでエラーを try-catch したい…
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
} catch (error) {
// ここに来てほしい…
return <p>読み込みに失敗しました…</p>;
}
}

これ、React 19 では NG 扱い です 🧨

React の公式ドキュメントには、

usetry-catch の中で呼ぶと「Suspense Exception: This is not a real error!」みたいなエラーになるから、 try-catch ではなく エラーバウンダリPromise の .catch を使ってね

という注意書きがあります。(React)

さらに、公式の ESLint ルールでも、

「レンダー中に起きたエラーは try-catch では拾えないから、 エラーバウンダリを使いましょう

とハッキリ書かれています。(React)

なぜ try-catch じゃダメなの?

  • コンポーネントの関数自体を 呼んでいるのは React 本体 だから
  • レンダリング中に起きたエラーは、React が「上方向」に伝播させてエラーバウンダリに届ける 仕組みになっているから(Epic React)

なので、私たちがコンポーネントの中で try { const data = use(...) } catch { ... } としても、 その catch にはエラーが落ちてこない、というイメージです 🌀


3️⃣ 図でイメージしよう 🧠✨

use(Promise) の世界で、エラーとローディングがどう流れるかの図です。

  • 「待ち」の時 → Suspense が担当
  • 「失敗」の時 → エラーバウンダリが担当

try-catch の出番はここにはありません 🙅‍♀️


4️⃣ エラー処理の選択肢はざっくり 2 パターン 🌈

React 19 の use ベースの世界では、だいたい次の 2 パターンで考えます。

✅ パターンA:エラーを「Promise 内側」で処理する(.catch パターン)

「エラーが出ても、エラー画面にはしたくない。 代わりに『データなし』として表示したい」 みたいなケースです。

React 公式の use ドキュメントでも、 「Promise の .catch で別の値に変換する」パターンが紹介されています。(React)

例:エラーのときは「空配列」にしてしまう

// api.ts
export type User = {
id: number;
name: string;
};

export function fetchUsers(): Promise<User[]> {
return fetch("https://example.com/api/users").then((res) => {
if (!res.ok) {
throw new Error("ユーザー取得に失敗しました");
}
return res.json() as Promise<User[]>;
});
}
// UserList.tsx
import { use } from "react";
import type { User } from "./api";
import { fetchUsers } from "./api";

export function UserList() {
// ❗ エラーが出たら空配列にしてしまう
const userPromise: Promise<User[]> = fetchUsers().catch((error) => {
console.error("ユーザー取得エラー", error);
// 「データなし」として扱う
return [];
});

const users = use(userPromise);

if (users.length === 0) {
return <p>ユーザーが見つかりませんでした。</p>;
}

return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

この書き方だと:

  • ネットワークエラーが起きても Promise の中で .catch して、
  • コンポーネントに届くときには 「ちゃんと resolve 済み」 になっています
  • なので use(userPromise)エラーを投げず、普通に動く

👉 「エラー画面」ではなく 「データがない通常状態」として扱いたいときにピッタリです。


✅ パターンB:エラーはエラーバウンダリに任せる(次章で本格的に✨)

もうひとつの考え方は、

エラーが起きたら、その部分の UI を「まるごとエラー画面」に切り替えたい 🧱

というパターンです。

そのときは Promise に .catch を付けずに、あえて reject のまま にしておきます。 すると、use が throw した Error が 一番近いエラーバウンダリ に届きます。(Medium)

イメージコード(エラーバウンダリの中身は次章で!)

// UserList.tsx(エラーは握りつぶさない)
import { use } from "react";
import { fetchUsers } from "./api";

export function UserList() {
const users = use(fetchUsers()); // ← エラーが起きたら Error を throw する

return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// App.tsx(イメージ)
import { Suspense } from "react";
import { UserList } from "./UserList";
import { AppErrorBoundary } from "./AppErrorBoundary"; // 119〜120章で実装予定

export function App() {
return (
<AppErrorBoundary>
<Suspense fallback={<p>ユーザー一覧を読込中…⏳</p>}>
<UserList />
</Suspense>
</AppErrorBoundary>
);
}

ここでは:

  • 読み込み中 → Suspensefallback(ローディング表示)
  • エラー → AppErrorBoundary がキャッチして「ごめんね画面」を表示

というコンビプレイになります ⚾


5️⃣ 「try-catch の代わりに何を使うの?」を整理しよう 📝

React のレンダリング中に起きるエラーについて、ざっくりまとめると:

  • ダメ

    • コンポーネントの中で try { const data = use(...); } catch { ... }
    • → React 19 では公式ドキュメント的にも非推奨/エラー扱い (React)
  • OK 1:Promise 側で処理

    • fetchSomething().catch(() => 代わりの値を返す)
    • → コンポーネントには「成功済みの値」として渡す
  • OK 2:エラーバウンダリに任せる

    • Promise はそのまま(エラーは reject のまま)
    • use() が Error を throw → エラーバウンダリがキャッチ
  • OK 3:イベントハンドラの中の try-catch

    • onClick の中など、ふつうの JS と同じタイミング のものは try-catch でOK
    • これは「レンダー中のエラー」とは別物です

6️⃣ ちょこっと練習問題 ✏️💡

時間があるときに、以下をやってみてください:

  1. 今まで作った useSuspense のサンプル(もしあれば)で、

    • わざと fetch の URL を間違えてエラーにする
    • .catch なし → エラーバウンダリでエラー UI(※次章で実装)
    • .catch あり → 「データなし」として扱う この2パターンを比べてみる
  2. どんな画面なら

    • 「ちゃんとエラー画面を出したい」か
    • 「エラーでも、静かに空の結果として扱いたい」か を、自分なりに考えてメモしてみる 💭

7️⃣ この章のまとめ 🎀

  • React 19 の use(Promise) では、

    • pendingSuspense が担当
    • rejected → エラーバウンダリが担当
  • レンダリング中のエラーは、ふつうの try-catch では拾えない → エラーバウンダリPromise の .catch を使う世界観 ✨(React)

  • 「エラーを画面に出すか?」「静かに別の値に置き換えるか?」を UI のデザインとして決める のが大事 🎨

次の 第119章 では、 ここでチラ見せした 「エラーバウンダリってなに?どう書くの?」 を 実際のコードでしっかり作っていきます 🧱✨お楽しみに〜! 🎉