コンテンツにスキップ

Chapter 13: エラー処理

データの更新ができるようになると,失敗したときの備えが欠かせません。この章では JavaScript の try/catch と Next.js のエラーバウンダリ API を使い,ユーザー体験を損ねないエラーハンドリングを実装します。


この章で扱う内容

  • 特別な error.tsx ファイルを使い,ルートセグメント内のエラーを捕捉して,ユーザーにフォールバック UI を表示する方法。
  • notFound 関数not-found ファイル を使い,存在しないリソースに対して 404 を返す方法。

Server Actions に try/catch を追加する

まずは JavaScript の try/catch を Server Actions に追加して,エラーを丁寧に処理できるようにします。自分で書ける場合はそのまま実装して構いません。以下は例です。

/app/lib/actions.ts
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  const amountInCents = amount * 100;
  const date = new Date().toISOString().split('T')[0];

  try {
    await sql`
      INSERT INTO invoices (customer_id, amount, status, date)
      VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
    `;
  } catch (error) {
    // ひとまずコンソールに記録しておく
    console.error(error);
    return {
      message: 'Database Error: Failed to Create Invoice.',
    };
  }

  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}
/app/lib/actions.ts
export async function updateInvoice(id: string, formData: FormData) {
  const { customerId, amount, status } = UpdateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });

  const amountInCents = amount * 100;

  try {
    await sql`
        UPDATE invoices
        SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status}
        WHERE id = ${id}
      `;
  } catch (error) {
    // ひとまずコンソールに記録しておく
    console.error(error);
    return { message: 'Database Error: Failed to Update Invoice.' };
  }

  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

redirect は try/catch の外で呼んでいる点に注意してください。redirect は内部的に例外を投げて遷移を行うため,catch 節で捕捉されないようにする必要があります。try が成功した場合のみ到達する位置に置きます。

ここでは,DB 側の問題を 捕捉してユーザーに分かりやすいメッセージを返す という,グレースフルなエラー処理にしています。

未捕捉の例外が発生したらどうなるでしょうか。deleteInvoice アクションで明示的にエラーを投げてシミュレートしてみます。

/app/lib/actions.ts
export async function deleteInvoice(id: string) {
  throw new Error('Failed to Delete Invoice');

  // 到達しないコード
  await sql`DELETE FROM invoices WHERE id = ${id}`;
  revalidatePath('/dashboard/invoices');
}

この状態で請求書の削除を試すと,ローカルホストにエラーが表示されます。本番環境では,予期しないエラーが起きたときにもユーザーに丁寧な UI を見せたいはずです。そこで error.tsx を使います。なお,このテスト用の throw は次のステップに進む前に必ず削除してください。


error.tsx でルート配下の未捕捉エラーを扱う

error.tsx はルートセグメント用の UI 境界 を定義する特別なファイルです。予期しないエラーをキャッチし,ユーザーにフォールバック UI を表示できます。

/dashboard/invoices フォルダに error.tsx を作成し,以下を貼り付けます。

/dashboard/invoices/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // 必要に応じてエラーレポートサービスに送る
    console.error(error);
  }, [error]);

  return (
    <main className="flex h-full flex-col items-center justify-center">
      <h2 className="text-center">Something went wrong!</h2>
      <button
        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
        onClick={
          // エラーバウンダリをリセットしてルートを再レンダリング
          () => reset()
        }
      >
        Try again
      </button>
    </main>
  );
}

ポイント:

  • 先頭に "use client" が必要です(クライアントコンポーネント)。
  • error はネイティブの Error インスタンス,resetエラーバウンダリをリセットして再レンダリングを試みる関数です。

これで再度削除を試すと,フォールバック UI が表示されます。

alt text


notFoundnot-found.tsx による 404 の扱い

error.tsx は未捕捉例外のキャッチに有用ですが,存在しないリソースを扱うときは notFound を使って 404 を明示するのが適切です。

例として,以下の URL にアクセスしてみます(存在しない UUID):

http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit

今は /invoices 配下に error.tsx があるため,この子ルートでエラー UI が表示されます。しかし,ここは 404 を返すのが望ましいケースです。

data.tsfetchInvoiceById で戻り値をログすると,存在しない場合は空配列であることが分かります。

/app/lib/data.ts
export async function fetchInvoiceById(id: string) {
  try {
    // ...
    console.log(invoice); // [] が返る
    return invoice[0];
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch invoice.');
  }
}

/dashboard/invoices/[id]/edit/page.tsxnotFound を使いましょう。

/dashboard/invoices/[id]/edit/page.tsx
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { notFound } from 'next/navigation';

export default async function Page(props: { params: Promise<{ id: string }> }) {
  const params = await props.params;
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);

  if (!invoice) {
    notFound();
  }

  // ...
}

そして,ユーザーに見せる 404 UI を not-found.tsx で定義します(/edit フォルダ内)。

alt text

/dashboard/invoices/[id]/edit/not-found.tsx
import Link from 'next/link';
import { FaceFrownIcon } from '@heroicons/react/24/outline';

export default function NotFound() {
  return (
    <main className="flex h-full flex-col items-center justify-center gap-2">
      <FaceFrownIcon className="w-10 text-gray-400" />
      <h2 className="text-xl font-semibold">404 Not Found</h2>
      <p>Could not find the requested invoice.</p>
      <Link
        href="/dashboard/invoices"
        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
      >
        Go Back
      </Link>
    </main>
  );
}

リロードすると 404 ページが表示されるはずです。

alt text

覚えておきたいのは,notFound()error.tsx より優先される点です。より 特定の状況を明示したいときに notFound() を使うとよいでしょう。


参考資料