Error yang tidak ditangkap menjatuhkan seluruh UI. Error boundary membatasi ledakan ke area kecil — dan graceful degradation memastikan fitur rusak tidak merusak fitur lain.
React Error Boundary:
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { contexts: { react: errorInfo } });
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong</div>;
}
return this.props.children;
}
}
// Penggunaan
<ErrorBoundary fallback={<FeatureUnavailable />}>
<ChartWidget />
</ErrorBoundary>
Apa yang DITANGKAP error boundary:
- Error saat render
- Error di lifecycle methods
- Error di constructor children
Apa yang TIDAK ditangkap:
- Event handlers (pakai try/catch biasa)
- Async code (setTimeout, promises, fetch)
- Server-side rendering errors
- Error di error boundary itu sendiri
Strategy placement — scope yang tepat:
❌ Satu error boundary di root — seluruh app blank saat satu widget error ✅ Error boundary per feature/section — widget rusak tidak merusak page
<App>
<ErrorBoundary fallback={<NavFallback />}>
<Navigation />
</ErrorBoundary>
<ErrorBoundary fallback={<MainFallback />}>
<Sidebar />
<ErrorBoundary fallback={<WidgetFallback />}>
<Chart />
</ErrorBoundary>
</ErrorBoundary>
</App>
Async error pattern:
function useSafeAsync(asyncFn) {
const [state, setState] = useState({ loading: true, data: null, error: null });
useEffect(() => {
let cancelled = false;
asyncFn()
.then(data => !cancelled && setState({ loading: false, data, error: null }))
.catch(error => !cancelled && setState({ loading: false, data: null, error }));
return () => { cancelled = true; };
}, [asyncFn]);
return state;
}
Graceful degradation — fitur rusak, app tetap jalan:
// Bad: satu API gagal → halaman gagal
const [user, posts, notifications] = await Promise.all([
fetchUser(), fetchPosts(), fetchNotifications()
]);
// Good: independent, fallback per resource
const [userR, postsR, notifR] = await Promise.allSettled([
fetchUser(), fetchPosts(), fetchNotifications()
]);
const user = userR.status === "fulfilled" ? userR.value : null;
const posts = postsR.status === "fulfilled" ? postsR.value : [];
const notifications = notifR.status === "fulfilled" ? notifR.value : [];
Feature flag untuk kill switch:
if (featureFlags.enableNewChart && !globalErrors.chart) {
return <NewChart />;
}
return <OldChart />;
// Jika new chart crash 10x → kill flag otomatis
Retry dengan exponential backoff:
async function fetchWithRetry(url, maxAttempts = 3) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fetch(url);
} catch (err) {
if (attempt === maxAttempts - 1) throw err;
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
}
}
}
Prinsip desain:
- Fail closed, not open — jika authz gagal di-check, tolak akses (jangan kasih akses)
- Fail visible, not silent — tampilkan pesan user, log ke monitoring
- Preserve user data — jangan hilang draft saat error
- Degrade gracefully — fitur non-critical rusak → app tetap usable
- Error message manusiawi — "Gagal memuat data. Coba lagi?" bukan "AxiosError: 500"
Monitoring yang penting:
- Error rate per feature (naik mendadak → new bug)
- Error rate per user cohort (browser tertentu? versi tertentu?)
- Time-to-recovery (seberapa cepat auto-recover?)