TOOLS BOX/ガイド/エラーハンドリングの考え方
Concept

エラーハンドリングの考え方

Next.js / React アプリでの失敗の扱い方。期待される失敗(バリデーション・認証失敗)は UI に返し、予期しない失敗(DB障害・システムエラー)は throw して error.tsx でキャッチする。2 種類の失敗を区別することが判断の出発点。

nextjsreacterrorerror-boundaryserver-actionstry-catchnot-founderror.tsx

どういう場面で使うか

  • ·フォーム送信失敗・バリデーションエラー・認証失敗 → return { error } で UI に返す
  • ·DB障害・外部API障害・予期しない例外 → throw して error.tsx / error boundary でキャッチさせる
  • ·リソースが存在しない → notFound() を呼んで 404 ページを表示する

注意点 / Pitfalls

  • ·すべての失敗を throw すると、バリデーションエラーのたびにページ全体がエラー画面になる
  • ·すべての失敗を return { error } で返すと、DB 障害などのシステムエラーがサイレントになりユーザーが気づかない
  • ·Server Actions 内で try/catch を書くとき、redirect() の throw まで catch してしまわないよう redirect は try の外に出す
  • ·error.tsx は 'use client' が必要。Server Component では使えない

補足

失敗を 2 種類に分けること(期待される失敗 / 予期しない失敗)が判断の起点。期待される失敗はユーザーに回復手段を提供でき、予期しない失敗はシステム側の問題でユーザーには回復できない。

失敗を 2 種類に分ける

Next.js / React アプリの失敗は 期待される失敗予期しない失敗 の 2 種類に分けることが判断の出発点です。

期待される失敗予期しない失敗
バリデーションエラー・認証失敗・not foundDB 障害・外部 API 障害・プログラムのバグ
起因ユーザー操作・ビジネスルールシステム側の問題
ユーザーは回復できるかできる(入力を直すなど)できない
扱い方return { error } で UI に返すthrow して error.tsx にキャッチさせる

期待される失敗 — UI に返す

バリデーションエラーや認証失敗のように、ユーザーが操作を修正すれば解決できる失敗は、エラーメッセージとして UI に返します。

// Server Action
"use server";
export async function loginAction(
  _prev: { error?: string } | null,
  formData: FormData
) {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  // バリデーション(期待される失敗)
  if (!email || !password) {
    return { error: "メールアドレスとパスワードを入力してください" };
  }

  const ok = await authenticate(email, password);
  if (!ok) {
    return { error: "メールアドレスまたはパスワードが正しくありません" };
  }

  redirect("/dashboard");
}
// Client Component(useActionState で状態を受け取る)
"use client";
import { useActionState } from "react";
import { loginAction } from "./actions";

export function LoginForm() {
  const [state, action, isPending] = useActionState(loginAction, null);
  return (
    <form action={action}>
      {state?.error && (
        <p className="text-sm text-red-500">{state.error}</p>
      )}
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button disabled={isPending}>ログイン</button>
    </form>
  );
}

予期しない失敗 — throw して error.tsx にキャッチさせる

DB 障害や外部 API の失敗など、ユーザーが回復できないエラーは throw します。Next.js の error.tsx がキャッチして、フォールバック UI を表示します。

// Server Action(DB エラーは throw させる)
"use server";
export async function createPostAction(formData: FormData) {
  const title = formData.get("title") as string;
  if (!title) {
    return { error: "タイトルを入力してください" }; // 期待される失敗 → 返す
  }

  // DB エラーは catch せずに throw させる → error.tsx がキャッチ
  await db.post.create({ data: { title } });
  revalidatePath("/posts");
  redirect("/posts");
}
// app/posts/error.tsx
"use client"; // error.tsx は必ず Client Component
import { useEffect } from "react";

export default function ErrorPage({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    console.error(error); // エラーを外部サービスに送信するなど
  }, [error]);

  return (
    <div>
      <h2>問題が発生しました</h2>
      <button onClick={reset}>再試行</button>
    </div>
  );
}

not-found — リソースが存在しない場合

データベースのレコードが存在しない場合は notFound() を呼んで 404 ページを表示します。エラーではなく「存在しない」という正常な応答です。

// app/posts/[slug]/page.tsx
import { notFound } from "next/navigation";

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await db.post.findUnique({ where: { slug: params.slug } });

  if (!post) {
    notFound(); // → app/not-found.tsx を表示
  }

  return <PostDetail post={post} />;
}
// app/not-found.tsx(任意でカスタマイズ)
export default function NotFound() {
  return (
    <div>
      <h2>ページが見つかりません</h2>
      <a href="/">トップへ戻る</a>
    </div>
  );
}

Server Actions でのエラー区分まとめ

Server Action 内の失敗
├── バリデーションエラー・ビジネスルール違反
│   └── return { error: "..." }  ← useActionState の state で受け取る
├── リソースが存在しない
│   └── notFound()  ← not-found.tsx を表示
├── 認証失敗(ページ遷移が必要な場合)
│   └── redirect("/login")  ← try/catch の外で呼ぶ
└── DB 障害・外部 API 障害・予期しない例外
    └── throw(または catch せず伝播)← error.tsx がキャッチ

よくある誤用

すべてを try/catch で握りつぶす

// ❌ DB エラーも return で返すと、システム障害がサイレントになる
export async function createPost(formData: FormData) {
  try {
    await db.post.create({ ... });
  } catch (e) {
    return { error: "失敗しました" }; // DB 障害も同じ扱いになってしまう
  }
}

// ✅ 期待される失敗だけ catch して返す。DB エラーは throw させる
export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  if (!title) return { error: "タイトルを入力してください" };

  // DB エラーは伝播させて error.tsx にキャッチさせる
  await db.post.create({ data: { title } });
}

redirect() を try/catch に入れる

// ❌ redirect() の throw が catch に捕まってリダイレクトされない
try {
  await doSomething();
  redirect("/done");
} catch (e) {
  return { error: "失敗" }; // redirect の NEXT_REDIRECT もここに入る
}

// ✅ redirect() は try/catch の外
try {
  await doSomething();
} catch (e) {
  return { error: "失敗" };
}
redirect("/done");

useActionState でエラーを状態として受け取る方法は → useActionState

Server Actions の更新フロー全体は → Server Actions

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例