← back to Govarbitrage

apps/mobile/components/ErrorBoundary.tsx

62 lines

/**
 * Root RENDER-error net. Scope (be honest about it): React error boundaries
 * catch errors thrown during RENDER / lifecycle of descendants only — they do
 * NOT catch errors in event handlers, promises, or async code (those are
 * handled by local try/catch in the fetch/auth paths). Without this, a
 * render-phase throw unmounts the whole tree to a blank screen; this shows a
 * branded fallback + Retry instead. Retry clears the error and re-renders the
 * children, so it recovers a TRANSIENT error; a deterministic one (e.g. a bad
 * API payload) re-throws — the durable fix for those is validating the payload
 * at the api.ts boundary, not this net.
 */
import React from "react";
import { StyleSheet, View } from "react-native";
import { Colors } from "../constants/theme";
import { ErrorCard } from "./ErrorCard";

interface Props {
  children: React.ReactNode;
}

interface State {
  hasError: boolean;
  message: string;
}

export class ErrorBoundary extends React.Component<Props, State> {
  state: State = { hasError: false, message: "" };

  static getDerivedStateFromError(error: unknown): State {
    return {
      hasError: true,
      message: error instanceof Error && error.message ? error.message : "An unexpected error occurred.",
    };
  }

  componentDidCatch(error: unknown, info: unknown): void {
    // Diagnostics only — no external telemetry is wired in this app.
    console.error("[ErrorBoundary]", error, info);
  }

  reset = (): void => this.setState({ hasError: false, message: "" });

  render(): React.ReactNode {
    if (this.state.hasError) {
      return (
        <View style={styles.fallback}>
          <ErrorCard title="Something went wrong" message={this.state.message} onRetry={this.reset} />
        </View>
      );
    }
    return this.props.children;
  }
}

const styles = StyleSheet.create({
  fallback: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: Colors.bg,
  },
});