Next.js Server Actionsでのエラーハンドリングと型安全なレスポンス設計

Next.js React TypeScript Backend
結論

予想されるエラーは return 値で表現し、redirect()try/catch の外側で呼ぶのが公式ルールです。

// Next.js公式仕様:Server Actions でのエラーハンドリング
'use server';

import { redirect } from 'next/navigation';
import { z } from 'zod';

type ActionResult = { success: boolean; error?: string };

export async function createUser(prevState: ActionResult, formData: FormData): Promise<ActionResult> {
  let isSuccess = false;

  try {
    const email = formData.get('email');
    if (!email) {
      return { success: false, error: 'メールアドレスは必須です' }; // Expected Error
    }
    // DB登録処理...
    isSuccess = true;
  } catch (err) {
    // Uncaught Exceptions
    return { success: false, error: 'システムエラーが発生しました' };
  }

  // ⭕ redirect() は必ず try/catch の外側で呼び出す
  if (isSuccess) {
    redirect('/dashboard');
  }
}

Next.js公式仕様:2種類のエラー分類

Next.js公式ドキュメント(nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)では、エラーを2種類に分類してハンドリングすることを推奨しています。

エラー分類対象例ハンドリング手法
Expected Errors (予想されるエラー)フォームバリデーション失敗、パスワード不一致throw せず return { error } のオブジェクト値として返却
Uncaught Exceptions (予期せぬ例外)DB接続切断、サードパーティAPI障害try/catch でキャッチするか、error.tsx 境界へ委譲

実際に起こる事故:try/catch 内での redirect() 呼び出し罠

Next.jsの redirect() 関数は、内部的に特殊な例外エラー(NEXT_REDIRECT)を throw することでフレームワークのページ遷移を起動させる仕様になっています。

そのため、以下のように try ブロックの中で redirect() を呼び出すと、catch ブロックがリダイレクト用例外を捕獲してしまい、ページ遷移が一切動作しなくなる障害 が発生します。

// ❌ 事故:redirect が catch に捕まりリダイレクトしないダメな例
export async function myAction() {
  try {
    // 処理...
    redirect('/dashboard'); // ❌ 内部で throw される例外が下の catch に入ってしまう!
  } catch (err) {
    return { error: 'エラー発生' };
  }
}

実装手順

  1. Server Action 内で入力値チェック(Zod等)を行い、不備がある場合は return { success: false, error: '文言' } を返す
  2. DB更新などの通信処理のみを try...catch で囲む
  3. 成功後のリダイレクト処理 redirect('/path') は、必ず try...catch ブロックのスコープ外(外側)で実行する
  4. クライアント側(Form)では React 19 の useActionState フックを使ってエラーメッセージを表示する