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…

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:

Apa yang TIDAK ditangkap:

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:

  1. Fail closed, not open — jika authz gagal di-check, tolak akses (jangan kasih akses)
  2. Fail visible, not silent — tampilkan pesan user, log ke monitoring
  3. Preserve user data — jangan hilang draft saat error
  4. Degrade gracefully — fitur non-critical rusak → app tetap usable
  5. Error message manusiawi — "Gagal memuat data. Coba lagi?" bukan "AxiosError: 500"

Monitoring yang penting: