ゼロから学ぶ React, Next.js⑳【Learn Next.js】Chapter13
【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関数
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関数
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関数
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.' };
+ }
}
redirect
がtry/catch
ブロックの外で呼び出されていることに注目してください。これは、redirect
がエラーを投げることで動作し、catch
ブロックでキャッチされてしまうためです。これを避けるため、try/catch
の後にredirect
を呼び出すことができます。redirect
は、try
が成功した場合にのみ到達可能です。
メモ:redirectをtry/catchの中で呼び出すと
以下のようにあえてredirect
をtry/catch
の中で呼び出してみます。
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ボタンを押すとターミナルに以下のエラーが表示されます。
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アクションの関数の先頭でエラーを投げることができます。
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
という新しいファイルを作成し、以下のコードを貼り付けてください。
'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が表示されるはずです。
notFound関数で404エラーを処理する
エラーを適切に処理するもう1つの方法は、notFound関数を使用することです。error.tsxはすべてのエラーをキャッチするのに便利ですが、存在しないリソースを取得しようとしたときは、notFoundを使用できます。
例えば、http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit にアクセスしてみてください。
これは、データベースに存在しない偽のUUIDです。
すぐにerror.tsx
が動作するのがわかると思います。これは、[id]/edit
がerror.tsx
の定義されている/invoices
の子ルートだからです。
もっと具体的なエラーにしたい場合は、ユーザーがアクセスしようとしているリソースが見つからないことを伝えるために、404エラーを表示することができます。
data.ts
のfetchInvoiceById
関数に入り、返されたinvoice
をコンソールに出力することで、リソースが見つからないことを確認できます。
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
を呼び出すようにします。
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
ファイルを作成します。
そして、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が表示されるはずです。
覚えておくべきことは、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でのエラー処理について詳しく知るには、以下のドキュメントを参照してください。
次の章
Discussion