Solving Unhandled Promise Rejection in React Native
Solving Unhandled Promise Rejection
~9 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
"Possible Unhandled Promise Rejection" is one of the most common warnings in React Native apps – it appears when a Promise fails ("rejects") but NO .catch() or try/catch handles that error.
1. Description
EVERY async function implicitly returns a promise, and every await call can fail (network error, invalid JSON response, denied permission). If the error stays unhandled, React Native logs a warning – in the worst case, the app gets stuck in an unexpected state (e.g. a loading spinner that never disappears).
2. Short example
// WRONG: no catch, the error silently vanishes
async function load() {
const response = await fetch(url); // throws on network error
}
// RIGHT:
async function load() {
try {
const response = await fetch(url);
} catch (error) {
console.error(error);
}
}3. Complete project: robust data loading
npx create-expo-app unhandled-rejection-demo
cd unhandled-rejection-demoimport { useState, useEffect } from 'react';
import { View, Text, Button, ActivityIndicator, StyleSheet } from 'react-native';
async function loadQuote(shouldFail) {
const response = await fetch(
shouldFail ? 'https://invalid-address.example/nothing' : 'https://api.quotable.io/random'
);
if (!response.ok) {
throw new Error(`Server responded with status ${response.status}`);
}
return response.json();
}
export default function App() {
const [quote, setQuote] = useState(null);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function reload(shouldFail = false) {
setLoading(true);
setErrorMessage(null);
// try/catch handles BOTH network errors AND the manual throw above -
// without this try/catch, every failure case would become an unhandled rejection
try {
const data = await loadQuote(shouldFail);
setQuote(data.content);
} catch (error) {
console.error('Failed to load quote:', error.message);
setErrorMessage('Failed to load quote. Please try again.');
} finally {
// finally ALWAYS runs, success or failure - loading state is guaranteed to end
setLoading(false);
}
}
useEffect(() => {
reload();
}, []);
return (
<View style={styles.container}>
{loading && <ActivityIndicator />}
{quote && <Text style={styles.quote}>"{quote}"</Text>}
{errorMessage && <Text style={styles.error}>{errorMessage}</Text>}
<View style={styles.buttons}>
<Button title="New quote" onPress={() => reload(false)} />
<Button title="Simulate error" onPress={() => reload(true)} color="#dc2626" />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 100, paddingHorizontal: 24 },
quote: { fontSize: 16, fontStyle: 'italic', marginBottom: 20 },
error: { color: '#dc2626', marginBottom: 20 },
buttons: { gap: 12 },
});4. Explanation
- EVERY
awaitcall that CAN fail sits inside atryblock – that's the only reliable way to avoid unhandled rejections inasyncfunctions. finallyguaranteessetLoading(false)is ALWAYS called – a very common bug withoutfinally: the loading spinner stays visible forever on an error, because the reset in the success path is "forgotten" and never reached on the error path.- The manual
throw new Error(...)on a non-successful HTTP status is needed becausefetch()itself (unlike Axios) does NOT automatically reject on 4xx/5xx responses (see the "Sending a POST Request" chapter). - In a production app, you'd additionally register a global fallback (e.g. via
ErrorUtils.setGlobalHandler()) that at least centrally logs TRULY missed errors – a last safety net, not a replacement for localtry/catchblocks.