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>
getDerivedStateFromErrordipanggil React waktu ada anak yang melempar error saat render. State yang dia kembalikan membuat boundary menampilkanfallback.componentDidCatchtempat melaporkan error-nya, supaya tim tahu ada yang rusak.- Error boundary masih harus berupa class component, React 19 pun belum punya versi hook-nya. Kalau gak mau menulis sendiri, library
react-error-boundarymenyediakan komponen siap pakai.
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:
- Error di event handler, misalnya
onClick. - Kode async: callback
setTimeout, promise,fetch. - Server-side rendering (SSR).
- Error di dalam boundary itu sendiri (bukan di anaknya).
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 |
try/catchada di dalammap, jadi tiap widget punya penjaga sendiri. Footer tetap dirender walaupun Chart gagal.logdipanggil dengan object{ widget, error }, danerrordiisie.message(teksAPI down), bukan object error-nya.- Fallback-nya menyebut nama widget:
"[" + widget.name + " unavailable]".
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
})
- Yang gagal diganti
fallbacks[i], cadangan di posisi yang sama, jadi urutannya gak bergeser. - Dua saudaranya beda tujuan:
Promise.racemengambil yang paling cepat selesai (berhasil atau gagal),Promise.anymengambil yang pertama berhasil.
🎯 Prinsip yang dipakai tim
- Gagal kelihatan, bukan diam. Tampilkan fallback ke user, dan laporkan error-nya ke monitoring dari
componentDidCatch. - Pesan manusiawi. "Grafik gagal dimuat. Coba lagi?" lebih jelas dari "AxiosError: 500".
- Jangan buang data user. Draft yang sedang diketik tetap disimpan walaupun tombol Simpan error.
- Bisa pulih. Tombol Coba lagi yang me-reset boundary (
setState({ hasError: false })) membuat user gak perlu memuat ulang halaman.
⚠️ Jebakan yang sering ketemu
try/catchmembungkus seluruh loop. Begitu Chart gagal, loop berhenti dan Footer gak pernah dirender. Penjaganya harus di dalam, per widget.- Mencatat object error-nya.
log({ widget, error: e })membuatcalls[0].errorjadi object Error, bukan teksAPI down. Pakaie.message. - Membuang hasil yang gagal.
filterhasil yang fulfilled saja membuatresult[1]jadinotif, padahal harus[]. Ganti dengan cadangan di indeks yang sama. - Satu boundary saja di root. App gak jadi layar putih, tapi satu widget rusak tetap menghapus seluruh halaman.
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.