← 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,
},
});