Membangun aplikasi yang tangguh: menangani error di setiap lapisan (route handler, server action, UI), error boundaries dengan error.tsx, structured logging dengan Pino, dan observability dasar untuk mendiagnosis masalah di production.

Aplikasi yang baik bukan yang tidak pernah error — itu tidak ada. Aplikasi yang baik adalah yang menangani error dengan anggun dan meninggalkan jejak untuk didiagnosis. Episode ini membahas error handling & logging secara sistematis: di lapisan mana error bisa terjadi, bagaimana menanganinya, dan bagaimana mencatatnya agar bisa diselidiki setelahnya.
Fullstack berarti error bisa muncul di tiga dunia sekaligus: di server (query gagal), di jaringan (API eksternal down), dan di browser (JS crash). Kita butuh strategi di ketiganya.
Sudah kita pelajari di episode 5: return NextResponse.json dengan status code yang tepat dan jangan bocorkan detail internal. Pola lengkapnya:
import { NextResponse } from "next/server"
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params
try {
const note = await prisma.note.findUnique({ where: { id } })
if (!note) return NextResponse.json(
{ error: "Catatan tidak ditemukan" }, { status: 404 },
)
return NextResponse.json({ data: note })
} catch (error) {
logger.error({ error, id }, "gagal mengambil catatan")
return NextResponse.json(
{ error: "Terjadi kesalahan pada server" }, { status: 500 },
)
}
}Server action mengembalikan state error (pola useActionState episode 7). Aturan pentingnya: jangan melempar exception mentah ke UI. Tangkap, log, dan kembalikan pesan yang aman:
"use server"
export async function deleteNote(id: string) {
try {
await prisma.note.delete({ where: { id } })
revalidatePath("/notes")
return { ok: true }
} catch (error) {
logger.error({ error, id }, "gagal menghapus catatan")
return { ok: false, error: "Catatan gagal dihapus" }
}
}Untuk error yang terjadi saat render di browser, React dan Next.js menyediakan error boundaries. File error.tsx otomatis menjadi boundary untuk route-nya:
"use client"
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="py-12 text-center">
<h2 className="text-xl font-bold">Terjadi kesalahan</h2>
<p className="mt-2 text-gray-600">Tidak bisa memuat daftar catatan.</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-brand-600 text-white rounded"
>
Coba lagi
</button>
</div>
)
}error.tsx merender pengganti halaman yang gagal (layout tetap utuh), dan tombol reset mencoba render ulang. Untuk error yang merusak layout root, global-error.tsx adalah cadangan terakhir.
Tip
Jangan tampilkan error.message mentah ke pengguna — bisa berisi konteks internal (path, nama tabel, stack). Di development boleh, di production selalu tampilkan pesan generik. Detail teknisnya harus pergi ke log, bukan ke layar.
Route yang tidak ada mendapat perlakuan khusus via not-found.tsx — tanpa error boundary:
export default function NotFound() {
return (
<div className="py-24 text-center">
<h1 className="text-4xl font-bold">404</h1>
<p className="mt-2 text-gray-600">Halaman tidak ditemukan</p>
</div>
)
}console.log di production adalah resep bencana: tidak terstruktur, tidak punya level, dan tidak bisa di-filter. Ganti dengan structured logging — setiap baris log adalah JSON yang bisa dicari dan diproses alat lain:
pnpm add pinoimport { pino } from "pino"
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: {
service: "catatan-fullstack",
env: process.env.NODE_ENV,
},
})Pemakaiannya disiplin dengan context — kunci untuk debugging di production:
logger.info({ userId, action: "note.create" }, "catatan baru dibuat")
logger.warn({ userId, noteId }, "percobaan akses catatan orang lain")
logger.error({ error, noteId }, "query database gagal")Tiga hal yang membuat log berguna: level (info/warn/error), context terstruktur (object, bukan string), dan pesan singkat. Aturan emasnya: setiap log error harus cukup untuk di-reproduksi — logger.error("gagal") tidak berguna, logger.error({ error, noteId }) berguna.
Logging adalah fondasi observability, tapi belum lengkap. Untuk aplikasi fullstack kecil, mulai dari tiga metrik sederhana:
const start = performance.now()
// ...proses...
logger.info(
{ route: "/api/notes", ms: Math.round(performance.now() - start) },
"request selesai",
)Observability penuh — traces, metrics, dashboard — adalah topik episode 24. Untuk sekarang, fondasi logging yang rapi sudah jauh di depan kebanyakan aplikasi.
Warning
Jangan pernah log data sensitif: password, token, cookies, atau nomor kartu. Sekali token tercetak di log, ia harus dianggap bocor. Filter field sensitif sebelum log — ini bagian dari keamanan yang akan kita dalami di episode 16.
Selesaikan dengan urutan berikut:
src/lib/logger.ts dengan Pino dan gunakan di semua route handler + server action.error.tsx, global-error.tsx, dan not-found.tsx di aplikasi kalian.console.log dengan logger berkonteks dan berlevel.docker stop pg-fullstack
curl -s http://localhost:3000/notes -o /dev/null
docker start pg-fullstackLog di terminal harus menampilkan JSON error yang menyebutkan service, env, dan konteks — bukan undefined atau pesan kosong.
Inti yang harus dibawa pulang:
error.tsx untuk error render, global-error.tsx untuk cadangan, not-found.tsx untuk 404.console.log.Di episode 12 selanjutnya kita membahas file upload & storage — object storage, validasi file, dan streaming upload — untuk fitur unggah yang aman dan skalabel. Sampai jumpa di episode 12!