Next.jsのチュートリアルをやってみる【エラー処理編】

前回

Next.jsのチュートリアルをやってみる【データの変異編】

はじめに

前回はサーバーアクションを利用してデータを更新する方法を学びました。今回はJavaScriptのtry/catch文とNext.jsのAPIを使用してエラーを処理する方法を学んでいきます。

サーバーアクションにtry/catch文を追加

前回用意したサーバーアクションにtry/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})
    `;
  } catch (error) {
    return {
      message: 'Database Error: Failed to Create Invoice.',
    };
  }
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}

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

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.' };
  }
}

createInvoiceupdateInvoiceで使用されているredirectに注目するとtry/catch文の外で呼び出されています。それはredirectはエラーを投げてリダイレクトを実現しているため、catchブロックでキャッチされてしまうからです。これを避けるためにtry/catchのあとに呼び出しているというわけです。

では、サーバーアクションでエラーが投げられた場合に何が起こるかを実際に確認してみましょう。deleteInvoiceアクションの初めにエラーを投げるようにして、実行してみましょう。

実行確認した後はエラーを必ず削除してください。

/app/lib/actions.ts
export async function deleteInvoice(id: string) {
  throw new Error('Failed to Delete Invoice');
 
  // Unreachable code block
  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' };
  }
}

「エラーが出るとどうなるか」というのを確認するのは、想定外の動作をしてしまう潜在的な問題を早期発見することにつながるため、開発中に役立ちます。

突然エラーで動作不能にするのではなく、アプリケーションの実行を継続するようにエラーを表示させてみましょう。

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

ここでNext.jsの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="flec 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.tsxはクライアント・コンポーネントである必要がある。
  • 以下のpropsを受け入れる
    • error:このオブジェクトはJavaScriptのErrorオブジェクトのインスタンス。
    • reset:エラー境界をリセットする関数。実行するとルートセグメントの再描画を試みる。

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

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

たとえば、http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/editにアクセスしてみましょう。

このURLに含まれているUUIDは、データベースにはないUUIDを指定しています。

error.tsxが定義されている/invoiceの子ルートなのでerror.tsxがすぐに呼ばれますが、具体的なエラーにしたい場合は、404エラーという内容を表示させ、アクセスしようとしているリソースが見つからなかったことをユーザーに伝えることができます。

リソースが見つからなかったことは、data.tsfetchInvoiceById関数でログを出力することで確認することができます。

/app/lib/data.ts
export async function fetchInvoiceById(id: string) {
  noStore();
  try {
    // ...
 
    console.log(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に移動して以下の内容を追加します。

  1. 'next/navigation'から{ notFound }をインポートする。
  2. 請求書が存在しない場合は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();
  }
 
  // ...
}

これで請求書が見つからない場合はエラーを投げるようになりました。現在はNext.jsデフォルトのUIが表示されています。

独自のエラーUIを表示するには/dashboard/invoices/[id]/editフォルダ内にnot-found.tsxファイルを作成する必要があります。

/dashboard/invoices/[id]/editフォルダに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>
  );
}

まとめ

  • error.tsxを作成するとルートセグメントないの予期せぬエラーをキャッチすることができる
  • notFounderror.tsxよりも優先されるため、404エラーに最適。

次回

Next.jsのチュートリアルをやってみる【アクセシビリティの向上編】

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です