⚠️

ゼロから学ぶ React, Next.js⑳【Learn Next.js】Chapter13

2024/05/25に公開

【Chapter13】 エラーの処理

前の章では、Server Actionsを使ってデータを変更する方法を学びました。ここでは、JavaScriptのtry/catch文とNext.jsのAPIを使って、エラーを適切に処理する方法を見ていきます。

この章で扱うトピック

  • 🚫 ルートセグメントでエラーをキャッチし、ユーザーにフォールバックUIを表示するための特別なerror.tsxファイルの使い方。
  • ⚠️ notFound関数とnot-foundファイルを使用して、404エラー(存在しないリソース)を処理する方法。

Server Actionsにtry/catchを追加する

まず、JavaScriptのtry/catch文をServer Actionsに追加して、エラーを適切に処理できるようにしましょう。

やり方がわかる方は、数分かけてServer Actionsを更新するか、以下のコードをコピーしてください。

createInvoice関数
/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) {
+    return {
+      message: 'Database Error: Failed to Create Invoice.',
+    };
+  }
 
   revalidatePath('/dashboard/invoices');
   redirect('/dashboard/invoices');
 }
updateInvoice関数
/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) {
+    return { message: 'Database Error: Failed to Update Invoice.' };
+  }
 
   revalidatePath('/dashboard/invoices');
   redirect('/dashboard/invoices');
 }
deleteInvoice関数
/app/lib/actions.ts
 export async function deleteInvoice(id: string) {
+  try {
     await sql`DELETE FROM invoices WHERE id = ${id}`;
     revalidatePath('/dashboard/invoices');
     return { message: 'Deleted Invoice.' };
+  } catch (error) {
+    return { message: 'Database Error: Failed to Delete Invoice.' };
+  }
 }

redirecttry/catchブロックの外で呼び出されていることに注目してください。これは、redirectがエラーを投げることで動作し、catchブロックでキャッチされてしまうためです。これを避けるため、try/catchの後にredirectを呼び出すことができます。redirectは、tryが成功した場合にのみ到達可能です。

メモ:redirectをtry/catchの中で呼び出すと

以下のようにあえてredirecttry/catchの中で呼び出してみます。

/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})
 `;
+       revalidatePath('/dashboard/invoices');
+        redirect('/dashboard/invoices');
     } catch (error) {
+        if (error instanceof Error) {
+            console.error(error);
+        }
     }
 }

ブラウザでCreate Invoiceボタンを押すとターミナルに以下のエラーが表示されます。

terminal
Database Error: Error: NEXT_REDIRECT
    at getRedirectError (webpack-internal:///(rsc)/./node_modules/next/dist/client/components/redirect.js:44:19)
    at redirect (webpack-internal:///(rsc)/./node_modules/next/dist/client/components/redirect.js:54:11)
    at createInvoice (webpack-internal:///(rsc)/./app/lib/actions.ts:55:66)

エラー内容にあるnode_modules/next/dist/client/components/redirect.jsを見てみると

const REDIRECT_ERROR_CODE = "NEXT_REDIRECT";
var RedirectType;
(function(RedirectType) {
    RedirectType["push"] = "push";
    RedirectType["replace"] = "replace";
})(RedirectType || (RedirectType = {}));
function getRedirectError(url, type, permanent) {
    if (permanent === void 0) permanent = false;
    const error = new Error(REDIRECT_ERROR_CODE);
    error.digest = REDIRECT_ERROR_CODE + ";" + type + ";" + url + ";" + permanent;
    const requestStore = _requestasyncstorageexternal.requestAsyncStorage.getStore();
    if (requestStore) {
        error.mutableCookies = requestStore.mutableCookies;
    }
    return error;
}
function redirect(url, type) {
    if (type === void 0) type = "replace";
    throw getRedirectError(url, type, false);
}

redirect関数でgetRedirectErrorが呼ばれNEXT_REDIRECTエラーが投げられていることがわかります。
つまり、redirect関数はNEXT_REDIRECTエラーを投げるため、try/catchの中で呼び出すとそのエラーをcatchブロックでキャッチしてしまうため、処理が成功している場合でもエラーが表示されてしまう、ということです。

それでは、Server Actionでエラーが発生したときの動作を確認してみましょう。例えば、deleteInvoiceアクションの関数の先頭でエラーを投げることができます。

/app/lib/actions.ts
 export async function deleteInvoice(id: string) {
+  throw new Error('Failed to Delete Invoice');
 
   // 到達不可能なコードブロック
   try {
     await sql`DELETE FROM invoices WHERE id = ${id}`;
     revalidatePath('/dashboard/invoices');
     return { message: 'Deleted Invoice' };
   } catch (error) {
     return { message: 'Database Error: Failed to Delete Invoice' };
   }
 }

請求書を削除しようとすると、localhostにエラーが表示されるはずです。

開発中にこれらのエラーを見ることは、潜在的な問題を早期に発見するのに役立ちます。しかし、突然の失敗を避け、アプリケーションの実行を継続できるようにするためには、エラーをユーザーに表示することも必要です。

ここで、Next.jsのerror.tsxファイルの出番です。


error.tsxですべてのエラーを処理する

error.tsxファイルは、ルートセグメントのUIバウンダリを定義するために使用できます。これは予期しないエラーを全てキャッチし、ユーザーにフォールバック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={
          // invoicesルートの再レンダリングを試みてリカバリーする
          () => reset()
        }
      >
        Try again
      </button>
    </main>
  );
}

上記のコードについて、いくつか注意点があります。

  • "use client" - error.tsxはクライアントコンポーネントである必要があります。
  • 2つのプロパティを受け取ります。
    • error: このオブジェクトは、JavaScriptのネイティブErrorオブジェクトのインスタンスです。
    • reset: これは、エラーバウンダリをリセットするための関数です。実行されると、ルートセグメントの再レンダリングを試みます。

再度請求書の削除を試みると、以下のようなUIが表示されるはずです。

props を受け取るerror.tsxファイル


notFound関数で404エラーを処理する

エラーを適切に処理するもう1つの方法は、notFound関数を使用することです。error.tsxはすべてのエラーをキャッチするのに便利ですが、存在しないリソースを取得しようとしたときは、notFoundを使用できます。

例えば、http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit にアクセスしてみてください。

これは、データベースに存在しない偽のUUIDです。

すぐにerror.tsxが動作するのがわかると思います。これは、[id]/editerror.tsxの定義されている/invoicesの子ルートだからです。

もっと具体的なエラーにしたい場合は、ユーザーがアクセスしようとしているリソースが見つからないことを伝えるために、404エラーを表示することができます。

data.tsfetchInvoiceById関数に入り、返されたinvoiceをコンソールに出力することで、リソースが見つからないことを確認できます。

/app/lib/data.ts
 export async function fetchInvoiceById(id: string) {
   noStore();
   try {
     // ...
 
+    console.log(invoice); // invoiceは空の配列 []
     return invoice[0];
   } catch (error) {
     console.error('Database Error:', error);
     throw new Error('Failed to fetch invoice.');
   }
 }

請求書がデータベースに存在しないことがわかったので、notFoundを使ってそれを処理しましょう。/dashboard/invoices/[id]/edit/page.tsxに移動し、{ notFound }'next/navigation'からインポートします。

そして、条件分岐を使って、請求書が存在しない場合にnotFoundを呼び出すようにします。

/dashboard/invoices/[id]/edit/page.tsx
 import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
 import { updateInvoice } from '@/app/lib/actions';
+import { notFound } from 'next/navigation';
 
 export default async function Page({ params }: { params: { id: string } }) {
   const id = params.id;
   const [invoice, customers] = await Promise.all([
     fetchInvoiceById(id),
     fetchCustomers(),
   ]);
 
+  if (!invoice) {
+    notFound();
+  }
 
   // ...
 }

完璧です!<Page>は、特定の請求書が見つからない場合にエラーを投げるようになりました。ユーザーにエラーUIを表示するために、/editフォルダ内にnot-found.tsxファイルを作成します。

editフォルダ内のnot-found.tsxファイル

そして、not-found.tsxファイルの中に、次のコードを貼り付けてください。

/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>
  );
}

ルートを更新すると、以下のようなUIが表示されるはずです。

404 Not Found ページ

覚えておくべきことは、notFoundはerror.tsxよりも優先されるということです。より具体的なエラーを処理したい場合は、notFoundを使うことができます!

クイズの時間です!
知識をテストし、学んだことを確認しましょう。

Next.jsのどのファイルが、ルートセグメントの予期せぬエラーのキャッチオールとして機能しますか?

A. 404.tsx
B. not-found.tsx
C. error.tsx
D. catch-all.tsx

解答

C. error.tsx
error.tsxファイルは、予期せぬエラーの受け皿となり、ユーザーにフォールバックUIを表示することができます。


より詳しく学ぶ

Next.jsでのエラー処理について詳しく知るには、以下のドキュメントを参照してください。


次の章

https://zenn.dev/gunjo/articles/3507b76ceb8f83

Discussion