Next.js 16 Error Handling: คู่มือจัดการ Error ฉบับสมบูรณ์ พร้อมโค้ดตัวอย่าง

คู่มือจัดการ Error ใน Next.js 16 ครอบคลุม error.tsx, global-error.tsx, not-found.tsx และ Server Actions พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงสำหรับ production

บทนำ: ทำไม Error Handling ถึงสำคัญขนาดนี้

ลองนึกภาพดูครับ: คุณ deploy แอป Next.js ขึ้น production เสร็จเรียบร้อย ผู้ใช้คนหนึ่งกดปุ่มบันทึกข้อมูล แล้ว... จอขาวโพลน ไม่มี error message ไม่มีปุ่มกลับ ไม่มีอะไรเลย

ผมเคยเจอเหตุการณ์แบบนี้ตอนทำโปรเจกต์ให้ลูกค้า ตอนนั้นต้องรีบแก้กลางดึก ก็เลยเข้าใจเลยว่าการจัดการ error อย่างเป็นระบบมันสำคัญแค่ไหน

ใน Next.js 16 ระบบ error handling ถูกออกแบบมาค่อนข้างดีผ่านระบบไฟล์ (file-based convention) ที่ทำงานร่วมกับ React Error Boundaries ได้อย่างราบรื่น ไม่ว่าจะเป็น error.tsx สำหรับ route segment, global-error.tsx สำหรับ root layout หรือการจัดการ error ใน Server Actions ทั้งหมดนี้ถูกออกแบบมาให้ทำงานเป็นชั้นๆ แบบ defense-in-depth

งั้นเรามาเจาะลึกทุกแง่มุมของ error handling ใน Next.js 16 กันเลย พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงตั้งแต่พื้นฐานไปจนถึงรูปแบบขั้นสูงสำหรับ production

1. ภาพรวมระบบ Error Handling ใน Next.js 16

Next.js App Router แบ่ง error handling ออกเป็น 4 ระดับหลักครับ แต่ละระดับมีหน้าที่ชัดเจน:

  • error.tsx — จับ error ใน route segment และ children ทั้งหมด
  • global-error.tsx — จับ error ใน root layout และ template
  • not-found.tsx — จัดการ 404 เมื่อ resource ไม่มีอยู่จริง
  • Server Actions error handling — จัดการ error จาก form submission และ mutation

สิ่งสำคัญที่ต้องเข้าใจคือ error จะ "bubble up" ขึ้นไปหา error boundary ที่ใกล้ที่สุด ถ้าไม่เจอ error.tsx ใน segment เดียวกัน มันจะไต่ขึ้นไปหา parent จนถึง root ซึ่งตรงนี้เหมือนกับ try/catch ใน JavaScript ปกติเลย

ลำดับชั้นของ Error Boundary

app/
├── global-error.tsx    ← จับ error จาก root layout.tsx
├── layout.tsx          ← root layout
├── error.tsx           ← จับ error จาก page.tsx (ไม่รวม layout.tsx เดียวกัน)
├── page.tsx
├── not-found.tsx       ← จัดการ 404
└── dashboard/
    ├── error.tsx       ← จับ error เฉพาะ dashboard/*
    ├── layout.tsx
    └── page.tsx

จุดสำคัญ: error.tsx จะ ไม่ จับ error ที่เกิดใน layout.tsx ของ segment เดียวกัน เพราะ error boundary จะห่อหุ้มอยู่ ภายใน layout นั้น ถ้าอยากจับ error ใน layout ต้องวาง error.tsx ใน parent segment แทน ตรงนี้เป็นจุดที่หลายคนสับสนครับ

2. error.tsx — หัวใจหลักของ Error Boundary

error.tsx คือไฟล์ที่คุณจะใช้บ่อยที่สุดในการจัดการ error พอเกิด error ใน route segment ไฟล์นี้จะโชว์ fallback UI แทนที่ component tree ที่พังไป

กฎสำคัญ: ต้องเป็น Client Component เท่านั้น

error.tsx ต้องใส่ 'use client' ทุกครั้งนะครับ เพราะ React Error Boundaries อาศัย lifecycle method ของ class component (componentDidCatch) ซึ่งทำงานได้เฉพาะฝั่ง client ถ้าลืมใส่ error boundary จะไม่ทำงาน แล้วก็ไม่มี warning อะไรขึ้นมาด้วย ซึ่งค่อนข้างน่าหงุดหงิด

ตัวอย่าง error.tsx พื้นฐาน

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

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // ส่ง error ไปยังระบบ monitoring
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
      <h2 className="text-xl font-semibold text-red-600">
        เกิดข้อผิดพลาดบางอย่าง
      </h2>
      <p className="text-gray-600">
        {error.message || 'ไม่สามารถโหลดข้อมูลได้ กรุณาลองใหม่อีกครั้ง'}
      </p>
      {error.digest && (
        <p className="text-sm text-gray-400">
          Error ID: {error.digest}
        </p>
      )}
      <button
        onClick={() => reset()}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
      >
        ลองใหม่อีกครั้ง
      </button>
    </div>
  );
}

Props ที่ได้รับ

  • error — object ของ Error ที่เกิดขึ้น มี message (ข้อความ error) และ digest ซึ่งเป็น hash สำหรับ matching กับ server log
  • reset — ฟังก์ชันสำหรับ re-render error boundary ใหม่ ถ้าสำเร็จก็จะแสดง content ปกติแทน fallback UI

เรื่อง Security ที่ต้องรู้

ตรงนี้สำคัญมากครับ ใน production Next.js จะ ลบรายละเอียด error ที่ละเอียดอ่อน ออกจาก Server Component errors ก่อนส่งไปฝั่ง client โดย error.message จะเป็นข้อความกว้างๆ และ error.digest คือ hash ที่ใช้จับคู่กับ log ฝั่ง server ก็เพื่อป้องกันไม่ให้ข้อมูล sensitive หลุดไปถึง browser นั่นเอง

3. ฟังก์ชัน reset() — กู้คืนจาก Error

ฟังก์ชัน reset() คือเครื่องมือที่ให้ผู้ใช้ลอง recover จาก error ได้ พอเรียก reset() มันจะ re-render content ใน error boundary ถ้าปัญหาหายไปแล้ว (เช่น network กลับมาปกติ) UI ก็จะกลับมาแสดงได้ถูกต้อง

ข้อจำกัดของ reset() ที่ต้องรู้

แต่มีจุดที่ต้องระวังครับ reset() จะ re-render client component เท่านั้น มัน ไม่ trigger Server Component ให้ fetch ข้อมูลใหม่ ถ้าปัญหาอยู่ที่ข้อมูลจาก server คุณต้องใช้ router.refresh() ร่วมด้วย:

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

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  const router = useRouter();

  useEffect(() => {
    console.error(error);
  }, [error]);

  const handleRetry = () => {
    // refresh server data ก่อน
    router.refresh();
    // แล้วค่อย reset error boundary
    reset();
  };

  return (
    <div className="p-8 text-center">
      <h2 className="text-xl font-bold text-red-600 mb-4">
        เกิดข้อผิดพลาด
      </h2>
      <p className="text-gray-600 mb-6">{error.message}</p>
      <div className="flex gap-4 justify-center">
        <button
          onClick={handleRetry}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg"
        >
          ลองใหม่
        </button>
        <button
          onClick={() => router.push('/')}
          className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg"
        >
          กลับหน้าหลัก
        </button>
      </div>
    </div>
  );
}

เคล็ดลับ: reset() เหมาะกับ transient error อย่าง network timeout แต่ถ้าเป็น persistent error เช่น data ผิดรูปแบบ ก็ต้องแก้ที่ต้นเหตุจริงๆ ครับ กด reset กี่ทีก็ไม่หาย

4. global-error.tsx — ป้องกัน Error ระดับ Root Layout

error.tsx ในระดับ root (app/error.tsx) จับ error จาก page.tsx ได้ แต่จับ error จาก app/layout.tsx ไม่ได้นะครับ เพราะ error boundary อยู่ ภายใน layout เพื่อแก้ปัญหานี้ Next.js เลยมี global-error.tsx มาให้

ข้อแตกต่างสำคัญจาก error.tsx

  • global-error.tsx ต้องมี <html> และ <body> tags เพราะมัน replace layout ทั้งหมดเลย
  • ใน development จะเห็น error overlay แทน ต้อง test ด้วย next start (production mode) ถึงจะเห็นหน้านี้
  • ถูก trigger น้อยมาก เพราะ root layout มักจะเป็น static อยู่แล้ว
// app/global-error.tsx
'use client';

import { useEffect } from 'react';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // ส่ง error ไปยัง monitoring service เช่น Sentry
    console.error('Global error:', error);
  }, [error]);

  return (
    <html lang="th">
      <body>
        <div className="flex flex-col items-center justify-center min-h-screen">
          <h1 className="text-2xl font-bold text-red-600 mb-4">
            เกิดข้อผิดพลาดร้ายแรง
          </h1>
          <p className="text-gray-600 mb-6">
            แอปพลิเคชันไม่สามารถทำงานต่อได้ กรุณาลองรีเฟรชหน้าเว็บ
          </p>
          <button
            onClick={() => reset()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg"
          >
            ลองใหม่
          </button>
        </div>
      </body>
    </html>
  );
}

Best Practice: แม้ว่า global-error.tsx จะ trigger ไม่บ่อย แต่ตามประสบการณ์ของผม ทุกโปรเจกต์ ควรมี ทั้ง app/error.tsx และ app/global-error.tsx เอาไว้ครับ เผื่อเหตุไม่คาดฝัน ดีกว่าปล่อยให้ user เจอจอขาว

5. not-found.tsx — จัดการ 404 อย่างสง่างาม

not-found.tsx ถูกออกแบบมาเฉพาะสำหรับกรณีที่ resource ไม่มีอยู่จริง ซึ่งแตกต่างจาก error.tsx ที่จัดการ runtime error ทั่วไป ถ้าจะเปรียบเทียบก็เหมือน error.tsx เป็นตาข่ายรองรับทั่วไป ส่วน not-found.tsx คือตัวที่ดักเฉพาะ 404 โดยเฉพาะ

วิธีใช้ notFound()

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (!res.ok) return null;
  return res.json();
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  if (!post) {
    notFound(); // trigger not-found.tsx
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
// app/blog/[slug]/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div className="text-center py-20">
      <h2 className="text-3xl font-bold mb-4">ไม่พบบทความ</h2>
      <p className="text-gray-600 mb-6">
        บทความที่คุณกำลังค้นหาอาจถูกลบหรือย้ายไปแล้ว
      </p>
      <Link
        href="/blog"
        className="text-blue-600 hover:underline"
      >
        ← กลับไปหน้ารวมบทความ
      </Link>
    </div>
  );
}

จุดที่น่าสนใจ: notFound() มีลำดับความสำคัญสูงกว่า error.tsx นะครับ แล้วก็ not-found.tsx เป็น Server Component ได้ด้วย ไม่ต้องใส่ 'use client'

6. Error Handling ใน Server Actions — ส่วนที่ซับซ้อนที่สุด

พูดตรงๆ นะครับ Server Actions เป็นจุดที่ error handling ปวดหัวที่สุด เพราะโค้ดทำงานฝั่ง server แต่ผลลัพธ์ต้องแสดงฝั่ง client Next.js แนะนำ 2 แนวทางหลัก:

แนวทาง 1: Return Error เป็น Value (แนะนำ)

แทนที่จะ throw error ให้ return ผลลัพธ์เป็น object ที่มี status แบบนี้ครับ:

// app/actions/create-post.ts
'use server';

import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(1, 'กรุณาระบุหัวข้อ'),
  content: z.string().min(10, 'เนื้อหาต้องมีอย่างน้อย 10 ตัวอักษร'),
});

type ActionState = {
  success: boolean;
  message: string;
  errors?: Record<string, string[]>;
};

export async function createPost(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  // Validate input
  const validated = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!validated.success) {
    return {
      success: false,
      message: 'ข้อมูลไม่ถูกต้อง',
      errors: validated.error.flatten().fieldErrors,
    };
  }

  try {
    await db.posts.create({ data: validated.data });
    return { success: true, message: 'สร้างบทความสำเร็จ' };
  } catch (error) {
    return {
      success: false,
      message: 'ไม่สามารถสร้างบทความได้ กรุณาลองใหม่',
    };
  }
}
// app/blog/new/page.tsx (Client Component)
'use client';

import { useActionState } from 'react';
import { createPost } from '@/app/actions/create-post';

const initialState = { success: false, message: '' };

export default function NewPostForm() {
  const [state, formAction, isPending] = useActionState(
    createPost,
    initialState
  );

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">หัวข้อ</label>
        <input
          id="title"
          name="title"
          className="border rounded px-3 py-2 w-full"
        />
        {state.errors?.title && (
          <p className="text-red-500 text-sm mt-1">
            {state.errors.title[0]}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="content">เนื้อหา</label>
        <textarea
          id="content"
          name="content"
          rows={6}
          className="border rounded px-3 py-2 w-full"
        />
        {state.errors?.content && (
          <p className="text-red-500 text-sm mt-1">
            {state.errors.content[0]}
          </p>
        )}
      </div>

      {!state.success && state.message && (
        <p className="text-red-600 bg-red-50 p-3 rounded">
          {state.message}
        </p>
      )}

      {state.success && (
        <p className="text-green-600 bg-green-50 p-3 rounded">
          {state.message}
        </p>
      )}

      <button
        type="submit"
        disabled={isPending}
        className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {isPending ? 'กำลังบันทึก...' : 'บันทึก'}
      </button>
    </form>
  );
}

แนวทางนี้ผมชอบเพราะ error ที่เกิดขึ้นจะ predictable มาก ฝั่ง client รู้เลยว่าจะได้รับ response format แบบไหน

แนวทาง 2: Throw Error → จับด้วย Error Boundary

สำหรับ unexpected error ที่ไม่สามารถ recover ได้ ก็ throw ไปเลยเพื่อ trigger error boundary:

// app/actions/delete-post.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function deletePost(postId: string) {
  const session = await getSession();
  if (!session) {
    throw new Error('ไม่ได้รับอนุญาต');
  }

  try {
    await db.posts.delete({ where: { id: postId } });
    revalidatePath('/blog');
  } catch (error) {
    throw new Error('ไม่สามารถลบบทความได้');
  }
}

ข้อควรระวัง: redirect() ต้องอยู่นอก try/catch

นี่คือ gotcha ที่คนเจอบ่อยมากครับ (รวมถึงตัวผมเองด้วย) redirect() ทำงานด้วยการ throw error ภายใน ถ้าเรียกใน try/catch มันจะถูก catch block ดักจับไว้ ทำให้ redirect ไม่ทำงาน:

// ❌ ผิด — redirect จะถูก catch ดักจับ
export async function createPost(formData: FormData) {
  try {
    await db.posts.create({ data: formData });
    redirect('/blog'); // จะถูก catch ดักจับ!
  } catch (error) {
    return { error: 'เกิดข้อผิดพลาด' };
  }
}

// ✅ ถูกต้อง — redirect อยู่นอก try/catch
export async function createPost(formData: FormData) {
  let success = false;

  try {
    await db.posts.create({ data: formData });
    success = true;
  } catch (error) {
    return { error: 'ไม่สามารถสร้างบทความได้' };
  }

  if (success) {
    redirect('/blog');
  }
}

7. Error Handling ใน Server Components

Server Components มีความท้าทายเฉพาะตัวเรื่อง error handling เพราะมันทำงานบน server แต่ React Error Boundaries ทำงานเฉพาะฝั่ง client อย่างไรก็ตาม คุณใช้ try/catch แบบ JavaScript ปกติได้เลย:

// app/dashboard/page.tsx (Server Component)
import { Suspense } from 'react';

async function DashboardStats() {
  try {
    const stats = await fetch('https://api.example.com/stats', {
      next: { revalidate: 60 },
    });

    if (!stats.ok) {
      throw new Error('ไม่สามารถโหลดสถิติได้');
    }

    const data = await stats.json();

    return (
      <div className="grid grid-cols-3 gap-4">
        <div className="p-4 bg-white rounded-lg shadow">
          <h3>ผู้ใช้ทั้งหมด</h3>
          <p className="text-2xl font-bold">{data.totalUsers}</p>
        </div>
        <div className="p-4 bg-white rounded-lg shadow">
          <h3>ยอดขายวันนี้</h3>
          <p className="text-2xl font-bold">{data.todaySales}</p>
        </div>
        <div className="p-4 bg-white rounded-lg shadow">
          <h3>คำสั่งซื้อใหม่</h3>
          <p className="text-2xl font-bold">{data.newOrders}</p>
        </div>
      </div>
    );
  } catch (error) {
    // แสดง fallback UI แทนที่จะ throw ต่อ
    return (
      <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
        <p className="text-yellow-800">
          ⚠ ไม่สามารถโหลดสถิติได้ในขณะนี้
        </p>
      </div>
    );
  }
}

export default function DashboardPage() {
  return (
    <div>
      <h1 className="text-2xl font-bold mb-6">แดชบอร์ด</h1>
      <Suspense fallback={<div>กำลังโหลดสถิติ...</div>}>
        <DashboardStats />
      </Suspense>
    </div>
  );
}

เลือกให้ถูกนะครับ: ถ้า error ใน Server Component ไม่ร้ายแรง ให้ catch แล้วแสดง fallback ภายใน component เอง แต่ถ้าร้ายแรงจริงๆ ให้ throw ออกมาเพื่อ trigger error.tsx แทน

8. จัดการ Error ใน Event Handlers

อันนี้หลายคนไม่รู้ครับ Error Boundary จับ error ใน event handler ไม่ได้ เพราะ event handler ทำงานหลังจาก rendering เสร็จแล้ว คุณต้องจัดการเองด้วย try/catch + useState:

// app/components/DeleteButton.tsx
'use client';

import { useState } from 'react';
import { deletePost } from '@/app/actions/delete-post';

export default function DeleteButton({ postId }: { postId: string }) {
  const [error, setError] = useState<string | null>(null);
  const [isDeleting, setIsDeleting] = useState(false);

  const handleDelete = async () => {
    setError(null);
    setIsDeleting(true);

    try {
      await deletePost(postId);
    } catch (err) {
      setError(
        err instanceof Error
          ? err.message
          : 'เกิดข้อผิดพลาดในการลบ'
      );
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div>
      {error && (
        <p className="text-red-500 text-sm mb-2">{error}</p>
      )}
      <button
        onClick={handleDelete}
        disabled={isDeleting}
        className="px-3 py-1 bg-red-600 text-white rounded text-sm disabled:opacity-50"
      >
        {isDeleting ? 'กำลังลบ...' : 'ลบ'}
      </button>
    </div>
  );
}

9. รูปแบบ Error Handling ขั้นสูงสำหรับ Production

สร้าง Reusable Error Component

ถ้าเขียน error UI ซ้ำๆ ทุกไฟล์ก็เหนื่อยครับ วิธีที่ดีกว่าคือสร้าง component กลางที่ใช้ร่วมกันได้:

// app/components/ErrorFallback.tsx
'use client';

import { useRouter } from 'next/navigation';

interface ErrorFallbackProps {
  error: Error & { digest?: string };
  reset: () => void;
  title?: string;
  showHomeButton?: boolean;
}

export default function ErrorFallback({
  error,
  reset,
  title = 'เกิดข้อผิดพลาด',
  showHomeButton = true,
}: ErrorFallbackProps) {
  const router = useRouter();

  const handleRetry = () => {
    router.refresh();
    reset();
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] p-8">
      <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
        <span className="text-red-600 text-2xl">!</span>
      </div>
      <h2 className="text-xl font-bold text-gray-900 mb-2">{title}</h2>
      <p className="text-gray-600 text-center max-w-md mb-6">
        {error.message || 'เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้ง'}
      </p>
      {error.digest && (
        <p className="text-xs text-gray-400 mb-4">
          Ref: {error.digest}
        </p>
      )}
      <div className="flex gap-3">
        <button
          onClick={handleRetry}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
        >
          ลองใหม่
        </button>
        {showHomeButton && (
          <button
            onClick={() => router.push('/')}
            className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
          >
            กลับหน้าหลัก
          </button>
        )}
      </div>
    </div>
  );
}
// app/dashboard/error.tsx — ใช้ ErrorFallback
'use client';

import ErrorFallback from '@/app/components/ErrorFallback';

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <ErrorFallback
      error={error}
      reset={reset}
      title="ไม่สามารถโหลดแดชบอร์ดได้"
    />
  );
}

เห็นไหมครับ แค่ 3 บรรทัดก็ได้ error page สำหรับแต่ละ route แล้ว สะอาดกว่าเขียนซ้ำๆ เยอะ

เชื่อมต่อกับ Error Monitoring (Sentry)

สำหรับ production จำเป็นต้องมีระบบ monitoring ครับ จะได้รู้ว่า user เจอ error อะไรบ้าง Sentry เป็นตัวเลือกยอดนิยมที่รองรับ Next.js 16 ได้ดี:

// ติดตั้ง: npx @sentry/wizard -i nextjs

// app/dashboard/error.tsx — เวอร์ชันที่ส่ง error ไปยัง Sentry
'use client';

import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
import ErrorFallback from '@/app/components/ErrorFallback';

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <ErrorFallback
      error={error}
      reset={reset}
      title="ไม่สามารถโหลดแดชบอร์ดได้"
    />
  );
}

Sentry SDK ตัวล่าสุดรองรับ Turbopack (bundler ตั้งต้นของ Next.js 16) ได้เต็มรูปแบบ แล้วยังมีฟีเจอร์เด็ดๆ อย่าง Session Replay, Source Maps และ Breadcrumbs อีกด้วย

10. สรุป: ตารางเลือกใช้ Error Handling ที่ถูกต้อง

มาสรุปกันครับ ตารางนี้จะช่วยให้คุณเลือกใช้ error handling แต่ละประเภทได้ถูกต้องตามสถานการณ์:

สถานการณ์ วิธีที่แนะนำ
Expected error (validation, auth) Return { success: false, error } + useActionState
Unexpected error ใน route segment error.tsx
Error ใน root layout global-error.tsx
Resource ไม่มีอยู่ (404) notFound() + not-found.tsx
Error ใน event handler try/catch + useState
Non-critical Server Component error try/catch ภายใน component + fallback UI
Critical Server Component error throw → ถูกจับโดย error.tsx
Error monitoring Sentry + captureException ใน useEffect

สุดท้ายนี้ อยากบอกว่า error handling ที่ดีไม่ใช่แค่ catch error ได้ แต่ต้องให้ user experience ที่ดีด้วย ผู้ใช้ต้องรู้ว่าเกิดอะไรขึ้น ทำอะไรต่อได้ และไม่รู้สึกว่าติดอยู่ในจุดที่ไม่มีทางออก ลองนำแนวทางในบทความนี้ไปปรับใช้ดูนะครับ

คำถามที่พบบ่อย (FAQ)

error.tsx กับ global-error.tsx ต่างกันอย่างไร?

error.tsx จับ error ใน route segment และ children แต่จับ error ใน layout.tsx ของ segment เดียวกันไม่ได้ ส่วน global-error.tsx จับ error ใน root layout.tsx และ template.tsx โดยต้องมี <html> และ <body> tags เพราะมัน replace layout ทั้งหมด

ทำไม error.tsx ถึงต้องเป็น Client Component?

เพราะ React Error Boundaries อาศัย lifecycle method componentDidCatch ซึ่งทำงานได้เฉพาะฝั่ง client ดังนั้น error.tsx ต้องใส่ 'use client' เสมอ ถ้าลืมใส่ error boundary จะไม่ทำงานเงียบๆ โดยไม่มี warning ด้วย

reset() กับ router.refresh() ต่างกันอย่างไร ใช้อันไหนดี?

reset() จะ re-render client component ใน error boundary ใหม่ แต่ ไม่ trigger Server Component ให้ fetch ข้อมูลใหม่ ส่วน router.refresh() จะ invalidate server cache และ fetch ข้อมูลจาก server ใหม่ ส่วนใหญ่แนะนำให้ใช้ทั้งสองร่วมกันครับ เรียก router.refresh() ก่อน แล้วตามด้วย reset()

redirect() ใน Server Action ไม่ทำงาน ต้องทำอย่างไร?

redirect() ทำงานด้วยการ throw error ภายใน ถ้าเรียกใน try/catch มันจะถูก catch block ดักจับ วิธีแก้คือย้าย redirect() ออกไปอยู่นอก try/catch โดยใช้ตัวแปร flag เช็คก่อนว่า operation สำเร็จหรือไม่

ควรวาง error.tsx ที่ระดับไหนของ route hierarchy?

ขึ้นอยู่กับความต้องการครับ วาง error.tsx ที่ระดับ root (app/error.tsx) เพื่อจับ error กว้างๆ แล้วก็วางเพิ่มในแต่ละ route segment (เช่น app/dashboard/error.tsx) เพื่อแสดง error UI ที่เฉพาะเจาะจงสำหรับแต่ละส่วน อย่างน้อยควรมี app/error.tsx + app/global-error.tsx เป็นฐานครับ

เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.