React Native Error Handling

Every app meets failures: a lost connection, a server outage, an unexpected empty value, or a bug that slipped through testing. Users judge an app by how it behaves in those moments. A frozen screen or silent crash drives people away, while a clear message and a retry button keep trust intact.

Where Errors Come From

Source               Example
────────────────────────────────────────────
Network              No signal, timeout
Server               500 reply, wrong data format
User input           Invalid email, empty field
Device               Permission denied, low storage
Code bug             Reading a field of undefined

Layer 1: try and catch

Wrap risky async work in try and catch. Use finally for steps that must run either way, such as hiding a spinner.

const loadProfile = async () => {
  setLoading(true);
  setError(null);

  try {
    const response = await fetch('https://example.com/profile');
    if (!response.ok) {
      throw new Error(`Server returned ${response.status}`);
    }
    setProfile(await response.json());
  } catch (e) {
    setError('Could not load your profile. Check your connection.');
  } finally {
    setLoading(false);
  }
};

fetch does not throw on a 404 or 500 reply. Check response.ok and throw an error yourself.

Layer 2: Three Screen States

Every data screen has three possible faces: loading, error, and content. Design all three.

Loading           Error                 Content
┌──────────┐      ┌──────────────┐      ┌──────────┐
│    ◌     │      │ Could not    │      │ Item 1   │
│ Loading  │      │ load data    │      │ Item 2   │
│          │      │ [ Try again ]│      │ Item 3   │
└──────────┘      └──────────────┘      └──────────┘
if (loading) return <ActivityIndicator size="large" />;

if (error) {
  return (
    <View style={{ padding: 20, alignItems: 'center' }}>
      <Text>{error}</Text>
      <Button title="Try again" onPress={loadProfile} />
    </View>
  );
}

return <ProfileView data={profile} />;

Layer 3: Error Boundaries

A bug during rendering crashes the whole app. An error boundary catches the crash inside a section and shows a fallback instead. Error boundaries use a class component, since hooks have no equivalent.

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    reportError(error, info);   // send to a monitoring service
  }

  render() {
    if (this.state.hasError) {
      return (
        <View style={{ padding: 20 }}>
          <Text>Something went wrong.</Text>
          <Button title="Retry" onPress={() => this.setState({ hasError: false })} />
        </View>
      );
    }
    return this.props.children;
  }
}

<ErrorBoundary>
  <ProfileScreen />
</ErrorBoundary>
App
├── ErrorBoundary ── Feed        (crash shows fallback here only)
├── ErrorBoundary ── Profile
└── ErrorBoundary ── Settings

Boundaries catch rendering errors only. They miss errors inside event handlers and async code, so keep try and catch there.

Layer 4: Global Handlers

Unhandled promise failures can slip through. Log them centrally so none vanish.

ErrorUtils.setGlobalHandler((error, isFatal) => {
  reportError(error);
  if (isFatal) {
    Alert.alert('Unexpected error', 'Please restart the app.');
  }
});

Layer 5: Monitoring Services

Crashes on user phones never appear in your terminal. Services such as Sentry and Firebase Crashlytics collect error reports with stack traces, device models, and app versions. Review them after every release.

Writing Helpful Messages

Unhelpful                        Helpful
───────────────────              ─────────────────────────────────
"Error 500"                      "Our server is busy. Try again soon."
"Network request failed"         "No internet. Check your connection."
"Invalid"                        "Enter an email like name@mail.com"

Offer a Way Forward

  • Add a retry button to every failed load
  • Keep previously loaded data visible when a refresh fails
  • Save unsent form data so users never retype it
  • Detect offline status with the NetInfo library and show a banner

Protect User Privacy in Reports

Remove passwords, tokens, and personal details before sending error logs. Reports need technical clues, not private data.

Summary

Strong error handling works in layers. Use try and catch for async work, design loading, error, and content states for each screen, wrap sections in error boundaries, and log unhandled failures centrally. Send reports to a monitoring service, write plain-language messages, and always give users a next step.