Error Boundaries & Graceful Degradation — Debugging

Error yang tidak ditangkap menjatuhkan seluruh UI. Error boundary membatasi ledakan ke area kecil — dan graceful degradation memastikan fitur rusak tidak…

Graceful degradation artinya kalau satu bagian app rusak, bagian lain tetap jalan, dan bagian yang rusak menampilkan pengganti yang sopan. Di React, alatnya adalah error boundary: komponen yang menangkap error dari komponen di dalamnya waktu render, lalu menampilkan fallback (tampilan pengganti) alih-alih membuat seluruh halaman putih. Bayangin MCB (sekring) di rumah. Kalau cuma ada satu MCB untuk seluruh rumah, korsleting di dapur bikin semua ruangan gelap. Kalau tiap ruangan punya MCB sendiri, cuma dapur yang padam, kamar lain tetap terang.

🧪 Tebakan cepat: Dashboard punya tiga widget: Header, Chart, dan Footer. Chart melempar error waktu render, dan satu-satunya error boundary dipasang di root (paling luar). Apa yang dilihat user? Jawabannya ada di bagian "Satu di root atau satu per widget".

Error boundary: penangkap error waktu render

class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError(error) {
    return { hasError: true } // render berikutnya menampilkan fallback
  }

  componentDidCatch(error, info) {
    logError(error, info) // kirim ke monitoring, misalnya Sentry
  }

  render() {
    if (this.state.hasError) return this.props.fallback
    return this.props.children
  }
}

<ErrorBoundary fallback={<p>[Chart unavailable]</p>}>
  <Chart />
</ErrorBoundary>

Satu di root atau satu per widget

// Satu boundary di root
<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Header /> <Chart /> <Footer />
</ErrorBoundary>

// Satu boundary per widget
<ErrorBoundary fallback={<p>[Header unavailable]</p>}><Header /></ErrorBoundary>
<ErrorBoundary fallback={<p>[Chart unavailable]</p>}><Chart /></ErrorBoundary>
<ErrorBoundary fallback={<p>[Footer unavailable]</p>}><Footer /></ErrorBoundary>
Penempatan Chart melempar error Yang dilihat user
Satu di root ya cuma "Something went wrong", Header dan Footer ikut hilang
Per widget ya "Selamat Datang", "[Chart unavailable]", "© 2026"

Dengan boundary cuma di root, satu widget rusak menghapus semuanya: Header dan Footer ikut hilang padahal gak salah apa-apa. Itu jawaban tebakannya. Boundary per widget membuat "ledakannya" kecil. Boundary di root tetap berguna sebagai pengaman terakhir, untuk error yang lolos dari semua boundary lain.

Yang gak ditangkap error boundary

Error boundary cuma menangkap error yang terjadi waktu React sedang render komponen di dalamnya. Yang lolos:

Untuk event handler dan kode async, tangkap sendiri dengan try/catch, simpan pesannya di state, lalu tampilkan:

async function handleSave() {
  try {
    await saveDraft()
  } catch (e) {
    setError(e.message) // boundary gak akan tahu, jadi kita yang menampilkan
  }
}

Graceful degradation tanpa React: safelyRender

Idenya sama di kode biasa: pasang penjaga per bagian, bukan satu untuk semuanya. Ini yang kamu tulis di latihan:

function safelyRender(widgets, log) {
  return widgets.map((widget) => {
    try {
      return widget.render()
    } catch (e) {
      log({ widget: widget.name, error: e.message })
      return "[" + widget.name + " unavailable]"
    }
  })
}
Widget render() Hasil Yang dicatat log
Header "Selamat Datang" Selamat Datang
Chart melempar Error("API down") [Chart unavailable] { widget: "Chart", error: "API down" }
Footer "© 2026" © 2026

Data dari banyak API: Promise.all vs Promise.allSettled

Halaman yang memanggil tiga API sekaligus (user, posts, notifikasi) punya masalah yang sama. Promise.all langsung reject begitu satu promise gagal, jadi satu API mati membuat seluruh halaman gagal. Promise.allSettled menunggu semuanya selesai dan gak pernah reject. Hasilnya array berisi { status: "fulfilled", value } atau { status: "rejected", reason }, urutannya sama dengan urutan promise-nya.

async function allSettledFallback(promises, fallbacks) {
  const results = await Promise.allSettled(promises)
  return results.map((r, i) => (r.status === "fulfilled" ? r.value : fallbacks[i]))
}

allSettledFallback(
  [Promise.resolve("user"), Promise.reject("err"), Promise.resolve("notif")],
  [null, [], null],
).then((result) => {
  console.log(result[0])                 // user
  console.log(JSON.stringify(result[1])) // []
  console.log(result[2])                 // notif
})

🎯 Prinsip yang dipakai tim

⚠️ Jebakan yang sering ketemu

TL;DR: Error boundary (class component dengan getDerivedStateFromError dan componentDidCatch) menangkap error waktu render di komponen anaknya dan menampilkan fallback. Pasang satu per widget atau bagian, ditambah satu di root sebagai pengaman terakhir, supaya Chart yang rusak cuma mengganti dirinya dengan [Chart unavailable]. Boundary gak menangkap error di event handler, kode async, SSR, atau di dirinya sendiri: tangkap dengan try/catch dan simpan di state. Di kode biasa, safelyRender memasang try/catch di dalam map, mencatat { widget: name, error: e.message }, dan mengembalikan "[Nama unavailable]". Untuk banyak API, Promise.allSettled menunggu semuanya dan gak pernah reject, lalu yang gagal diganti cadangan di posisi yang sama, jadi hasilnya user, [], notif.

Yang akan kamu pelajari