Error Handling di Next.js 16: Panduan Lengkap error.tsx, global-error.tsx, dan not-found.tsx

Panduan lengkap error handling di Next.js 16 App Router: error.tsx, global-error.tsx, not-found.tsx, plus fitur baru forbidden() dan unauthorized() dengan contoh kode dan pola retry.

Error Handling Next.js 16: error.tsx (2026)

Diperbarui: 2 Agustus 2026

Error handling di Next.js 16 dilakukan melalui tiga file konvensi App Router: error.tsx untuk menangkap error di dalam sebuah segmen route, global-error.tsx untuk menangani error yang bocor sampai ke root layout, dan not-found.tsx yang di-render ketika notFound() dipanggil. Sejak Next.js 15.1, ada juga forbidden.tsx dan unauthorized.tsx untuk skenario otorisasi. Semua boundary ini dibangun di atas React Error Boundary, jadi mereka hanya menangkap error yang di-throw di komponen anak. Panduan ini mendemonstrasikan setiap file dengan kode yang bisa langsung dijalankan.

  • error.tsx adalah Client Component wajib yang menerima props error dan reset, dan hanya menangkap error dari segmen anak (bukan dari layout di segmen yang sama).
  • global-error.tsx menggantikan seluruh root layout (termasuk tag <html> dan <body>) dan hanya aktif di production build.
  • notFound() melempar error khusus yang di-tangkap oleh not-found.tsx terdekat. Sejak Next.js 15.1 ada juga forbidden() dan unauthorized() dengan konvensi file yang serupa.
  • Properti error.digest adalah hash deterministik yang aman dikirim ke client untuk keperluan support ticket tanpa membocorkan stack trace.
  • Fungsi reset() memicu re-render boundary (bukan reload halaman), jadi ideal untuk tombol "Coba lagi" pada error sementara.

Dasar error handling di App Router

Kalau kamu datang dari Pages Router, kejutan pertama adalah tidak ada lagi _error.js atau getInitialProps. App Router menggunakan pendekatan berbasis React Error Boundary, tapi Next.js membungkusnya menjadi konvensi file per-segmen. Setiap route segment (folder) bisa memiliki file error.tsx yang otomatis membungkus page.tsx di segmen tersebut plus semua segmen anaknya.

Yang perlu dipahami sejak awal: error boundary di React (dan karena itu di Next.js) hanya menangkap error rendering, error lifecycle, dan error di constructor. Mereka tidak menangkap error dari event handler, kode asynchronous seperti setTimeout, atau error server-side yang di-throw selama Server Component streaming. Untuk kasus itu kamu butuh try/catch manual atau Suspense boundary. Sederhana kedengarannya, tapi jujur, saya pernah menghabiskan setengah hari mencari kenapa error.tsx tidak nyala. Ternyata errornya lahir di onClick.

Ada lima entity utama yang saling bekerja sama di sistem error handling Next.js 16:

  • error.tsx: boundary per-segmen, wajib Client Component.
  • global-error.tsx: fallback tingkat root, menggantikan <html> dan <body>.
  • not-found.tsx: dipicu oleh helper notFound().
  • forbidden.tsx dan unauthorized.tsx: helper baru untuk error 403/401.
  • instrumentation.ts: hook untuk mengirim error ke observability tool seperti Sentry atau OpenTelemetry.

Sebelum masuk ke kode, pastikan kamu sudah memahami perbedaan Server dan Client Component. Kalau belum, kami sudah menulis panduan data fetching dan caching di Next.js 16 yang menjelaskan model rendering App Router secara mendalam.

Cara kerja error.tsx

File error.tsx selalu berupa Client Component. Next.js akan berteriak kalau kamu lupa direktif "use client". Alasannya sederhana: React Error Boundary butuh state dan lifecycle di client untuk menyimpan status error dan memicu reset(). File ini menerima dua props: error (object Error dengan properti tambahan digest) dan reset (fungsi untuk mencoba render ulang segmen).

Contoh minimal untuk app/dashboard/error.tsx:

"use client";

import { useEffect } from "react";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Kirim ke tool observability. Digest akan cocok dengan log server.
    console.error("Dashboard error:", error);
  }, [error]);

  return (
    <div className="rounded-lg border border-red-300 bg-red-50 p-6">
      <h2 className="text-lg font-semibold text-red-900">
        Terjadi masalah saat memuat dashboard
      </h2>
      <p className="mt-2 text-sm text-red-700">
        Kode referensi: {error.digest ?? "unknown"}
      </p>
      <button
        onClick={() => reset()}
        className="mt-4 rounded bg-red-600 px-4 py-2 text-white"
      >
        Coba lagi
      </button>
    </div>
  );
}

Yang sering bikin saya bingung ketika baru pindah ke App Router adalah cakupan boundary ini. error.tsx di app/dashboard/error.tsx tidak menangkap error yang di-throw oleh app/dashboard/layout.tsx. Layout dan error file berada di level yang sama, sehingga secara arsitektur error dari layout akan bubble ke boundary orang tua (biasanya global-error.tsx). Kalau kamu butuh menangkap error dari layout dashboard, taruh error.tsx satu level di atasnya, atau bungkus konten layout dengan Error Boundary manual.

Perbedaan error.tsx dan global-error.tsx

Pertanyaan yang sering muncul: kapan pakai error.tsx dan kapan pakai global-error.tsx? Jawaban singkatnya, gunakan error.tsx di setiap segmen route yang punya kemungkinan gagal (misalnya route yang melakukan fetch data eksternal), dan pakai global-error.tsx sebagai jaring pengaman terakhir kalau bahkan root layout kamu crash.

Perbedaan teknis paling penting adalah scope. global-error.tsx harus mendefinisikan tag <html> dan <body>-nya sendiri, karena ketika file ini aktif, root layout tidak pernah ter-render. Ini juga berarti kamu tidak bisa mengandalkan provider (theme, i18n, analytics) yang biasanya di-mount di root layout.

"use client";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html lang="id">
      <body>
        <main className="flex min-h-screen items-center justify-center bg-slate-950 text-white">
          <div className="max-w-md text-center">
            <h1 className="text-2xl font-bold">Aplikasi mengalami gangguan</h1>
            <p className="mt-3 text-slate-300">
              Tim kami sudah menerima laporan otomatis (ID: {error.digest}).
            </p>
            <button
              onClick={reset}
              className="mt-6 rounded bg-white px-5 py-2 text-slate-900"
            >
              Muat ulang
            </button>
          </div>
        </main>
      </body>
    </html>
  );
}

Hal kedua yang perlu diingat: global-error.tsx hanya aktif di production build. Di mode dev, Next.js sengaja menampilkan overlay error React agar kamu bisa debug. Kalau ingin memverifikasi UI, jalankan next build && next start dan sengaja lempar error dari sebuah page.

Aspekerror.tsxglobal-error.tsx
ScopeSegmen route + anak-anaknyaSeluruh aplikasi (fallback root)
Menangkap error dari root layoutTidakYa
Harus render <html>/<body>TidakYa
Aktif di mode devYaTidak (hanya production)
Akses ke provider root layoutYaTidak
Wajib "use client"YaYa

Menggunakan not-found.tsx dan notFound()

Untuk kasus resource yang tidak ada (misalnya artikel dengan slug yang tidak match), Next.js menyediakan helper notFound() yang bisa dipanggil dari Server Component, Route Handler, atau Server Action. Panggilan ini melempar error khusus NEXT_HTTP_ERROR_FALLBACK;404 yang otomatis ditangkap oleh not-found.tsx terdekat.

Contoh pola yang saya pakai untuk halaman detail artikel:

// app/artikel/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getArticleBySlug } from "@/lib/data";

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const article = await getArticleBySlug(slug);

  if (!article) {
    notFound(); // Melempar NEXT_HTTP_ERROR_FALLBACK;404
  }

  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  );
}

File app/artikel/[slug]/not-found.tsx di-render otomatis:

import Link from "next/link";

export default function ArticleNotFound() {
  return (
    <div className="mx-auto max-w-lg py-16 text-center">
      <h2 className="text-2xl font-semibold">Artikel tidak ditemukan</h2>
      <p className="mt-2 text-slate-600">
        Mungkin sudah diarsipkan atau URL-nya salah ketik.
      </p>
      <Link href="/artikel" className="mt-6 inline-block underline">
        Kembali ke daftar artikel
      </Link>
    </div>
  );
}

Yang membedakan not-found.tsx dari error.tsx: file ini adalah Server Component secara default, bisa async, dan menerima status HTTP 404 otomatis. Kalau kamu perlu menampilkan konten dinamis di halaman not-found (misalnya menyarankan artikel serupa), silakan lakukan fetch di komponen ini.

forbidden.tsx dan unauthorized.tsx (baru di 15.1)

Sejak Next.js 15.1 (dan lanjut di 16), tim Vercel menambahkan dua helper baru yang sering diminta komunitas: forbidden() untuk status 403 dan unauthorized() untuk 401. Sebelum ini, banyak developer meng-hack notFound() untuk kasus otorisasi, yang tidak semantik dan bikin log messy.

Untuk mengaktifkannya, tambahkan flag di next.config.ts:

// next.config.ts
import type { NextConfig } from "next";

const config: NextConfig = {
  experimental: {
    authInterrupts: true,
  },
};

export default config;

Kemudian di Server Component atau Server Action:

// app/admin/page.tsx
import { forbidden, unauthorized } from "next/navigation";
import { getSession } from "@/lib/auth";

export default async function AdminPage() {
  const session = await getSession();

  if (!session) {
    unauthorized(); // Render app/unauthorized.tsx, HTTP 401
  }

  if (session.role !== "admin") {
    forbidden(); // Render app/forbidden.tsx, HTTP 403
  }

  return <AdminDashboard user={session.user} />;
}

Pola ini bekerja sangat baik jika digabung dengan Data Access Layer. Untuk pola lengkap, kami sudah punya panduan autentikasi dan otorisasi di Next.js 16 yang menjelaskan cara memusatkan pengecekan otorisasi di satu tempat sehingga forbidden() hanya dipanggil dari lapisan data.

Fungsi reset() dan strategi retry

Prop reset yang diterima error.tsx memicu React untuk mencoba render ulang segmen yang error, tanpa full page reload. Ini penting untuk error transient seperti timeout jaringan atau rate limit sementara, di mana kita ingin memberi user tombol "Coba lagi" tanpa kehilangan state di komponen lain (misalnya form yang sedang diisi di layout).

Tapi ada catatan penting: reset() tidak menghapus cache. Kalau error kamu berasal dari fetch yang gagal dan hasilnya di-cache oleh React cache() atau Data Cache Next.js, retry akan mendapatkan hasil yang sama. Untuk kasus ini, kombinasikan dengan router.refresh():

"use client";

import { useRouter } from "next/navigation";
import { useTransition } from "react";

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

  const handleRetry = () => {
    startTransition(() => {
      router.refresh(); // Invalidasi cache Server Component
      reset();          // Reset boundary React
    });
  };

  return (
    <div>
      <p>Gagal memuat data: {error.message}</p>
      <button onClick={handleRetry} disabled={isPending}>
        {isPending ? "Memuat ulang..." : "Coba lagi"}
      </button>
    </div>
  );
}

Kombinasi router.refresh() dan reset() adalah pola standar yang saya rekomendasikan untuk semua tombol retry di App Router. Kalau kamu hanya panggil reset(), boundary akan mencoba render ulang dengan data yang sudah tercache, dan sering kali error yang sama akan muncul lagi. Saya pernah menghabiskan dua jam debugging kejadian persis seperti ini di project e-commerce sebelum akhirnya sadar penyebabnya cache.

Logging error dengan digest dan instrumentation

Di production, Next.js secara otomatis meng-hash pesan error dan stack trace ke sebuah string 8 karakter yang tersimpan di error.digest. Nilai yang sama tercatat di log server, sehingga kamu bisa mencocokkan laporan user dengan entri log tanpa membocorkan detail internal ke browser. Ini adalah cara aman untuk menampilkan "kode referensi" di UI error.

Untuk observability lebih dalam, gunakan file instrumentation.ts di root project. Sejak Next.js 15, file ini tersedia stabil dan diekspor sebagai satu-satunya cara resmi untuk mengaitkan Sentry, OpenTelemetry, atau tool APM lainnya. Contoh integrasi Sentry:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./sentry.server.config");
  }
  if (process.env.NEXT_RUNTIME === "edge") {
    await import("./sentry.edge.config");
  }
}

export async function onRequestError(
  err: unknown,
  request: {
    path: string;
    method: string;
    headers: Record<string, string>;
  },
  context: {
    routerKind: "Pages Router" | "App Router";
    routePath: string;
    routeType: "render" | "route" | "action" | "middleware";
    renderSource: "react-server-components" | "react-server-components-payload" | "server-rendering";
    revalidateReason: "on-demand" | "stale" | undefined;
    renderType: "dynamic" | "dynamic-resume";
  }
) {
  const Sentry = await import("@sentry/nextjs");
  Sentry.captureRequestError(err, request, context);
}

Hook onRequestError adalah salah satu API paling underrated di Next.js 16. Ia memberi kamu konteks lengkap (jalur, tipe render, sumber render, alasan revalidasi) yang mustahil didapat kalau hanya mengandalkan useEffect di error.tsx. Untuk detail lengkap, lihat dokumentasi instrumentation resmi Next.js.

Kesalahan umum dan cara mengatasinya

error.tsx tidak menangkap error saya

Alasan paling umum: error terjadi di event handler atau di code async yang tidak masuk render tree. Contoh, onClick yang throw error tidak akan ditangkap oleh error.tsx. Solusi: bungkus dengan try/catch manual dan panggil throw error di dalam useEffect, atau gunakan library seperti react-error-boundary. Untuk Server Action, throw error akan bubble ke boundary halaman yang memicu action, bukan ke boundary form.

Error dari root layout membuat halaman blank

Kalau root layout kamu throw (misalnya karena provider gagal init), error.tsx di child route tidak akan terpanggil. Solusinya cuma satu: buat app/global-error.tsx. Ini satu-satunya fallback untuk skenario ini.

notFound() melempar error di Client Component

Helper notFound() hanya boleh dipanggil dari Server Component, Route Handler, dan Server Action. Di Client Component gunakan useRouter().replace('/not-found') atau throw error biasa yang ditangkap boundary.

reset() tidak melakukan apa-apa

Biasanya karena data yang bikin error di-cache. Kombinasikan dengan router.refresh() seperti contoh di atas, atau invalidasi tag cache secara eksplisit dengan revalidateTag() dari Server Action.

Sentry menangkap dua kali

Jika kamu masih memakai config lama sentry.server.config.ts di root dan onRequestError, error akan dilaporkan dua kali. Sejak Sentry SDK 8, semua reporting harus melalui onRequestError. Hapus handler captureException manual di error.tsx agar tidak duplikasi.

Pertanyaan yang sering diajukan

Bagaimana cara menangani error di Next.js 16 App Router?

Tempatkan file error.tsx (Client Component wajib) di setiap segmen route yang mungkin gagal. File ini otomatis membungkus page.tsx dan seluruh anaknya dengan React Error Boundary, menerima props error dan reset. Untuk error yang lolos hingga root layout, tambahkan app/global-error.tsx.

Apa perbedaan error.tsx dan global-error.tsx di Next.js?

error.tsx menangkap error di segmen tempatnya berada beserta segmen anak, tapi tidak menangkap error dari layout di segmen yang sama. global-error.tsx adalah fallback tingkat root yang menggantikan seluruh <html> dan <body>, hanya aktif di production build, dan menangkap error dari root layout.

Kapan saya harus menggunakan not-found.tsx?

Gunakan setiap kali kamu memanggil notFound() dari Server Component, Route Handler, atau Server Action (misalnya ketika slug resource tidak match di database). File not-found.tsx di root app/ juga berfungsi sebagai halaman 404 global untuk URL yang tidak match rute manapun.

Apa itu properti digest di error Next.js?

error.digest adalah hash 8 karakter deterministik yang di-generate Next.js di production. Nilai yang sama tercatat di log server, jadi kamu bisa mencocokkan "kode referensi" yang ditampilkan ke user dengan entry log tanpa membocorkan pesan atau stack trace asli.

Mengapa reset() tidak berhasil memuat ulang data saya?

Fungsi reset() hanya me-render ulang boundary React. Ia tidak menghapus cache Data Cache Next.js. Kombinasikan dengan router.refresh() dari useRouter() di dalam useTransition, atau invalidasi tag spesifik dengan revalidateTag() dari Server Action untuk memaksa fetch ulang.

Ben Howard
Tentang Penulis Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.