D
P
0

Next.js

Produksi Balik HTML Basi Setelah Deploy? Ternyata `Cannot read properties of undefined (reading 'toFixed')` Bikin SSR Crash

10 Juli 2026·4 menit baca
Produksi Balik HTML Basi Setelah Deploy? Ternyata `Cannot read properties of undefined (reading 'toFixed')` Bikin SSR Crash

Ada sebuah dashboard data finansial yang saya bangun buat klien, dan setiap kali habis deploy, sebagian pengunjung mengeluh melihat angka lama. Bukan semua, cuma sebagian, dan tidak konsisten. Refresh sekali dua kali kadang benar, kadang balik basi lagi. Pola seperti itu selalu menggoda saya untuk menyalahkan cache atau deploy yang setengah jalan. Saya sempat percaya itu memang masalah cache selama hampir satu hari penuh.

Saya purge cache CDN, saya redeploy, saya cek header Cache-Control, semuanya kelihatan benar. Tapi HTML basi tetap muncul sesekali. Yang bikin saya akhirnya berhenti menebak adalah membuka log produksi dan mencari yang bukan request 200. Di situ ada errornya, terselip di antara request sukses:

TypeError: Cannot read properties of undefined (reading 'toFixed')

Bukan masalah cache. Itu SSR yang crash.

Kenapa ini menyamar jadi masalah cache

Begitu SSR melempar exception saat merender sebuah route, Next.js tidak bisa menyelesaikan render halaman itu. Yang terjadi berikutnya persis yang bikin saya bingung: alih-alih menampilkan error mentah ke pengunjung, Next.js jatuh kembali ke prerender lama yang masih ada di cache. Jadi dari luar, gejalanya seolah "HTML basi setelah deploy", padahal akar masalahnya adalah render baru yang gagal diam-diam. Cache basi cuma korban, bukan pelaku.

Itu sebabnya intermitten. Request yang kebetulan melewati data yang bikin crash akan gagal dan menyajikan versi cache; request lain yang datanya aman berhasil me-render versi baru. Dua pengunjung, dua hasil, dari deploy yang sama.

Akar masalahnya: tipe bilang number, runtime bilang undefined

Komponen penampil angka saya ditulis dengan penuh keyakinan pada tipe. Kira-kira begini bentuknya:

function ChangeBadge({ pct }: { pct: number }) {
  return <span>{pct.toFixed(1)}%</span>;
}

Selama pengembangan, pct selalu number, jadi tsc senang dan saya juga. Masalahnya, data yang mengalir ke komponen ini datang dari batas eksternal: API pihak ketiga yang sesekali ngadat, dan override parsial dari CMS yang kadang mengosongkan field. Di titik-titik itu, nilai yang sampai ke komponen bukan number, tapi undefined. TypeScript tidak pernah bisa menangkap ini karena tipe di batas eksternal itu janji, bukan jaminan runtime. Begitu undefined.toFixed() dipanggil, exception terlempar, SSR abort, dan mekanisme fallback tadi menyajikan prerender basi.

toLocaleString() kena masalah yang sama persis. Komponen lain yang memformat angka besar dengan pemisah ribuan crash dengan pola identik begitu nilainya null atau undefined.

Perbaikannya

Ada dua bagian. Pertama, jujur soal tipe. Prop yang membawa data dari batas eksternal tidak boleh diketik number polos, karena runtime tidak menghormati janji itu. Saya lebarkan jadi number | null | undefined supaya TypeScript memaksa saya menangani kasus kosong:

function ChangeBadge({ pct }: { pct: number | null | undefined }) {
  if (pct == null || !Number.isFinite(pct)) return <span>—</span>;
  return <span>{pct.toFixed(1)}%</span>;
}

Guard pct == null || !Number.isFinite(pct) menangkap null, undefined, dan juga NaN sekaligus, dengan fallback em-dash supaya UI tetap masuk akal secara visual alih-alih meledak. Perhatikan == null yang saya pakai sengaja, karena dia cocok untuk null dan undefined sekaligus.

Kedua, saya tidak mau menulis guard yang sama berulang kali di setiap komponen angka. Saya ekstrak helper bersama supaya polanya konsisten dan tidak ada yang kelupaan:

// lib/format.ts
export function safeFixed(value: number | null | undefined, digits = 1): string {
  if (value == null || !Number.isFinite(value)) return "—";
  return value.toFixed(digits);
}
 
export function safePct(value: number | null | undefined, digits = 1): string {
  if (value == null || !Number.isFinite(value)) return "—";
  return `${value.toFixed(digits)}%`;
}

Setelah itu setiap komponen angka memanggil safeFixed() atau safePct(), dan satu titik ini yang menjaga semua batas. Tidak ada lagi .toFixed() telanjang di JSX.

Cara memastikan ini bukan cache sejak awal

Pelajaran diagnostik yang paling saya bawa: ketika produksi kelihatan "nyangkut" di HTML lama setelah deploy, jangan langsung percaya itu cache. Cache basi sering hanya gejala dari SSR yang crash di baliknya. Sebelum menghabiskan waktu purge dan redeploy, buka log produksi dan grep khusus crash SSR, cari TypeError dan pola reading '...'. Kalau ada, itu penyebabnya, dan tidak ada purge cache yang akan memperbaikinya.

Checklist

  • Prop yang datanya berasal dari API pihak ketiga atau CMS jangan diketik number polos; lebarkan ke number | null | undefined.
  • Guard sebelum tiap method numerik: value == null || !Number.isFinite(value), dengan fallback yang aman secara visual.
  • Ekstrak safeFixed() / safePct() bersama supaya tidak ada guard yang kelupaan.
  • Kalau produksi menyajikan HTML basi setelah deploy, cek log SSR untuk crash sebelum menyalahkan cache.
  • Ingat: tsc hijau tidak menjamin data runtime menghormati tipe di batas eksternal.