Noções Básicas de Tratamento de Erros
10 exemplos para você começar com Tratamento de Erros - 7 básicos e 3 intermediários.
Busque em todas as páginas da documentação
10 exemplos para você começar com Tratamento de Erros - 7 básicos e 3 intermediários.
Aplicativos móveis falham de maneiras que aplicativos web raramente o fazem - LTE instável, buscas em segundo plano e travamentos de módulos nativos. Comece com um projeto Expo TypeScript em branco para que cada exemplo abaixo possa substituir App.tsx e ser executado imediatamente.
npx create-expo-app@latest MyResilientApp --template blank-typescript
cd MyResilientApp
npx expo startOs exemplos 8 e 10 usam detecção de conectividade de rede. Instale o NetInfo uma vez:
npx expo install @react-native-community/netinfoFerramentas: Estes exemplos têm como alvo Expo SDK 57, React Native 0.86 e React 19.2.3. TypeScript (
.tsx) é usado em todo o código.
Duas camadas de erro aparecem ao longo desta seção:
| Camada | Captura | Ferramenta |
|---|---|---|
| Imperativa | fetch falho, Promises rejeitadas, JSON inválido, manipuladores de eventos lançados | try/catch + estado |
| Declarativa | Travamentos de renderização, props inválidas, acesso a undefined durante JSX | React Error Boundary |
Use ambas. try/catch não pode capturar erros lançados enquanto o React está renderizando; Error Boundaries não podem capturar erros dentro de funções async ou manipuladores onPress.
Envolva chamadas de rede em try/catch para que um erro lançado se torne um estado de UI em vez de uma rejeição não tratada.
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
type Post = { id: number; title: string };
async function fetchPosts(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=3");
if (!res.ok) throw new Error(`Request failed (${res.status})`);
return res.json();
}
export default function App() {
const [posts, setPosts] = useState<Post[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
setPosts(await fetchPosts());
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setLoading(false);
}
})();
}, []);
if (loading) return <ActivityIndicator style={{ marginTop: 48 }} />;
if (error) return <Text style={{ padding: 16, color: "#b91c1c" }}>{error}</Text>;
return (
<View style={{ padding: 16, gap: 8 }}>
{posts.map((p) => (
<Text key={p.id}>{p.title}</Text>
))}
</View>
);
}try/catch pertence ao limite onde o trabalho assíncrono encontra o estado do React - tipicamente dentro de useEffect ou um manipulador de eventosres.ok explicitamente; fetch só rejeita em falha de rede, não em HTTP 4xx/5xxfinally garante que loading seja limpo mesmo quando a requisição lança um erroerr instanceof Error antes de ler .message - valores lançados nem sempre são objetos ErrorRelacionado: UX de Falha de Rede - filas de tentativa e stale-while-revalidate | Mensagens de Erro para o Usuário - escrevendo mensagens sobre as quais os usuários podem agir
Lançamentos síncronos dentro de onPress ignoram Error Boundaries. Capture-os localmente.
import { useState } from "react";
import { Pressable, Text, View, StyleSheet } from "react-native";
function parseQuantity(input: string): number {
const value = Number(input);
if (!Number.isFinite(value) || value <= 0) {
throw new Error("Enter a positive number");
}
return value;
}
export default function App() {
const [qty, setQty] = useState("2");
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState<number | null>(null);
function handleCalculate() {
try {
setError(null);
setTotal(parseQuantity(qty) * 9.99);
} catch (err) {
setTotal(null);
setError(err instanceof Error ? err.message : "Invalid input");
}
}
return (
<View style={styles.container}>
<Text>Quantity: {qty}</Text>
<Pressable onPress={() => setQty("0")} style={styles.chip}>
<Text>Set invalid (0)</Text>
</Pressable>
<Pressable onPress={handleCalculate} style={styles.button}>
<Text style={styles.buttonText}>Calculate total</Text>
</Pressable>
{error && <Text style={styles.error}>{error}</Text>}
{total !== null && <Text>Total: ${total.toFixed(2)}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24, gap: 12 },
chip: { alignSelf: "flex-start", padding: 8, backgroundColor: "#e5e7eb", borderRadius: 8 },
button: { backgroundColor: "#2563eb", padding: 12, borderRadius: 8, alignItems: "center" },
buttonText: { color: "#fff", fontWeight: "600" },
error: { color: "#b91c1c" },
});"Enter a positive number") em vez de strings genéricas "Error"Relacionado: Mensagens de Erro para o Usuário - mensagens acionáveis vs falhas opacas
Uma união de status discriminada impede combinações de UI impossíveis, como mostrar um spinner e um banner de erro simultaneamente.
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
type Profile = { name: string; email: string };
type Status =
| { phase: "loading" }
| { phase: "error"; message: string }
| { phase: "success"; data: Profile };
async function loadProfile(): Promise<Profile> {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!res.ok) throw new Error("Could not load profile");
const json = await res.json();
return { name: json.name, email: json.email };
}
export default function App() {
const [status, setStatus] = useState<Status>({ phase: "loading" });
useEffect(() => {
loadProfile()
.then((data) => setStatus({ phase: "success", data }))
.catch((err) =>
setStatus({
phase: "error",
message: err instanceof Error ? err.message : "Unknown error",
}),
);
}, []);
switch (status.phase) {
case "loading":
return <ActivityIndicator style={{ marginTop: 48 }} />;
case "error":
return <Text style={{ padding: 16, color: "#b91c1c" }}>{status.message}</Text>;
case "success":
return (
<View style={{ padding: 16, gap: 4 }}>
<Text style={{ fontWeight: "600" }}>{status.data.name}</Text>
<Text style={{ color: "#6b7280" }}>{status.data.email}</Text>
</View>
);
}
}phase como discriminante permite que o TypeScript restrinja status.data apenas dentro do branch "success"status substitui booleanos paralelos loading, error e data que podem sair de sincronia.then/.catch é equivalente a try/catch dentro de um IIFE assíncrono - escolha o que for mais claro no efeitoView em branco é um modo de falhaRelacionado: UX de Falha de Rede - stale-while-revalidate quando erros são transitórios
React Error Boundaries devem ser componentes de classe. Eles capturam falhas de renderização em árvores filhas e mostram uma UI de fallback.
import React, { Component, type ReactNode } from "react";
import { Text, View } from "react-native";
type Props = { children: ReactNode };
type State = { hasError: boolean };
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("Boundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) {
return (
<View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
<Text style={{ fontWeight: "600" }}>This section failed to render.</Text>
</View>
);
}
return this.props.children;
}
}
function BrokenWidget() {
const config = undefined as unknown as { label: string };
return <Text>{config.label}</Text>; // lança erro durante a renderização
}
export default function App() {
return (
<ErrorBoundary>
<BrokenWidget />
</ErrorBoundary>
);
}getDerivedStateFromError muda a UI para o fallback; componentDidCatch é para log e telemetriauseErrorBoundary - um pequeno wrapper de classe continua sendo o padrão idiomáticoinfo.componentStack em componentDidCatch; ele aponta qual componente de tela travouRelacionado: Error Boundaries em RN - fallbacks em nível de tela e UI de recuperação
Redefina um boundary alterando uma key no wrapper para que o React remonte a subárvore falha.
import React, { Component, useState, type ReactNode } from "react";
import { Pressable, Text, View, StyleSheet } from "react-native";
type BoundaryProps = { children: ReactNode; onRetry?: () => void };
type BoundaryState = { hasError: boolean };
class ErrorBoundary extends Component<BoundaryProps, BoundaryState> {
state: BoundaryState = { hasError: false };
static getDerivedStateFromError(): BoundaryState {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<View style={styles.fallback}>
<Text style={styles.title}>Something went wrong</Text>
<Pressable onPress={this.props.onRetry} style={styles.button}>
<Text style={styles.buttonText}>Try again</Text>
</Pressable>
</View>
);
}
return this.props.children;
}
}
function FlakyChart({ shouldFail }: { shouldFail: boolean }) {
if (shouldFail) throw new Error("Chart render failed");
return <Text>Chart rendered successfully</Text>;
}
export default function App() {
const [attempt, setAttempt] = useState(0);
const shouldFail = attempt < 2; // falha duas vezes, tem sucesso na terceira tentativa
return (
<ErrorBoundary key={attempt} onRetry={() => setAttempt((n) => n + 1)}>
<FlakyChart shouldFail={shouldFail} />
</ErrorBoundary>
);
}
const styles = StyleSheet.create({
fallback: { flex: 1, justifyContent: "center", alignItems: "center", gap: 12 },
title: { fontWeight: "600" },
button: { backgroundColor: "#2563eb", paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 },
buttonText: { color: "#fff", fontWeight: "600" },
});key remonta o boundary e limpa hasError - chamar setState sozinho dentro do boundary não pode recuperarattempt) para que tanto o boundary quanto o filho sejam redefinidos juntosonRetry como uma prop para que o boundary permaneça reutilizável em diferentes telasRelacionado: Error Boundaries em RN - envolvendo navegadores e telas de abas
Um botão de tentar novamente deve reinvocar a função de fetch, não apenas ocultar o texto de erro.
import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Pressable, Text, View, StyleSheet } from "react-native";
type Weather = { temperature: string };
async function fetchWeather(): Promise<Weather> {
const res = await fetch("https://example.com/api/weather");
if (!res.ok) throw new Error("Weather service unavailable");
return res.json();
}
export default function App() {
const [data, setData] = useState<Weather | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [nonce, setNonce] = useState(0);
const reload = useCallback(() => setNonce((n) => n + 1), []);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchWeather()
.then((result) => {
if (!cancelled) setData(result);
})
.catch((err) => {
if (!cancelled) {
setData(null);
setError(err instanceof Error ? err.message : "Request failed");
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [nonce]);
if (loading) return <ActivityIndicator style={{ marginTop: 48 }} />;
if (error) {
return (
<View style={styles.center}>
<Text style={styles.error}>{error}</Text>
<Pressable onPress={reload} style={styles.button}>
<Text style={styles.buttonText}>Retry</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.center}>
<Text>Temperature: {data?.temperature}</Text>
</View>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: "center", alignItems: "center", gap: 12, padding: 24 },
error: { color: "#b91c1c", textAlign: "center" },
button: { backgroundColor: "#2563eb", paddingHorizontal: 24, paddingVertical: 10, borderRadius: 8 },
buttonText: { color: "#fff", fontWeight: "600" },
});nonce reativa o efeito de forma limpa - evita duplicar a lógica de fetch dentro do manipulador de tentativacancelled impede que respostas obsoletas substituam o estado após um duplo clique rápido em Retryerror e defina loading no início de cada tentativa para que a UI mostre feedback de progressoloading se você quiser evitar requisições duplicadas em andamentoRelacionado: UX de Falha de Rede - backoff exponencial e filas offline
Combine ambas as camadas: boundaries contêm falhas de renderização; try/catch lida com tudo o que é assíncrono.
import React, { Component, useEffect, useState, type ReactNode } from "react";
import { ActivityIndicator, Text, View } from "react-native";
class ScreenBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <Text style={{ padding: 16 }}>Screen crashed - boundary caught it.</Text>;
}
return this.props.children;
}
}
function UserListScreen() {
const [names, setNames] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error("Fetch failed");
const users = await res.json();
setNames(users.map((u: { name: string }) => u.name));
} catch (err) {
setError(err instanceof Error ? err.message : "Load failed");
} finally {
setLoading(false);
}
})();
}, []);
if (loading) return <ActivityIndicator style={{ marginTop: 48 }} />;
if (error) return <Text style={{ padding: 16, color: "#b91c1c" }}>{error}</Text>;
return (
<View style={{ padding: 16, gap: 4 }}>
{names.map((name) => (
<Text key={name}>{name}</Text>
))}
</View>
);
}
export default function App() {
return (
<ScreenBoundary>
<UserListScreen />
</ScreenBoundary>
);
}try/catch para efeitos e manipuladores; Error Boundary para renderização - a divisão é mecânica, não opcionalRelacionado: Error Boundaries em RN - posicionamento em torno de navegadores | Melhores Práticas de Error Boundaries - falhar contido, registrar ruidosamente
Exiba a perda de conectividade no nível do shell para que cada tela herde o mesmo contexto offline.
import { useEffect, useState, type ReactNode } from "react";
import { Text, View, StyleSheet } from "react-native";
import NetInfo, { type NetInfoState } from "@react-native-community/netinfo";
function OfflineBanner({ visible }: { visible: boolean }) {
if (!visible) return null;
return (
<View style={styles.banner}>
<Text style={styles.bannerText}>You are offline. Some actions may not work.</Text>
</View>
);
}
function AppShell({ children }: { children: ReactNode }) {
const [offline, setOffline] = useState(false);
useEffect(() => {
const sync = (state: NetInfoState) => {
setOffline(!(state.isConnected && state.isInternetReachable !== false));
};
const unsubscribe = NetInfo.addEventListener(sync);
NetInfo.fetch().then(sync);
return unsubscribe;
}, []);
return (
<View style={styles.shell}>
<OfflineBanner visible={offline} />
<View style={styles.content}>{children}</View>
</View>
);
}
export default function App() {
return (
<AppShell>
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Main content continues below the banner.</Text>
</View>
</AppShell>
);
}
const styles = StyleSheet.create({
shell: { flex: 1 },
banner: { backgroundColor: "#fef3c7", paddingVertical: 8, paddingHorizontal: 16 },
bannerText: { color: "#92400e", textAlign: "center", fontWeight: "500" },
content: { flex: 1 },
});NetInfo.addEventListener dispara quando o dispositivo transita entre Wi-Fi, LTE e modo aviãoisConnected e isInternetReachable - portais cativos podem relatar conectado, mas não alcançávelapp/_layout.tsx no Expo Router) para que ele persista entre a navegaçãoRelacionado: UX de Falha de Rede - filas offline e leituras em cache | Padrões de Degradação Graciosa - modos reduzidos sem conectividade
ErrorUtils.setGlobalHandler captura erros JS que escapam de todos os boundaries - registre-o uma vez no início do aplicativo.
import { useEffect } from "react";
import { Text, View } from "react-native";
type GlobalHandler = (error: Error, isFatal?: boolean) => void;
function installGlobalHandler() {
const g = globalThis as typeof globalThis & {
ErrorUtils?: {
getGlobalHandler: () => GlobalHandler;
setGlobalHandler: (handler: GlobalHandler) => void;
};
};
const ErrorUtils = g.ErrorUtils;
if (!ErrorUtils) return;
const defaultHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
// Envie para Sentry / Datadog / seu backend aqui
console.error("[global]", error.message, { isFatal });
// Preserve o redbox em desenvolvimento
defaultHandler(error, isFatal);
});
}
export default function App() {
useEffect(() => {
installGlobalHandler();
}, []);
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Global handler registered on mount.</Text>
</View>
);
}setGlobalHandler uma vez - tipicamente em app/_layout.tsx ou um bootstrap.ts dedicado importado antes da navegaçãodefaultHandler para que os desenvolvedores ainda vejam o redbox em __DEV__isFatal === true significa que o runtime pode desativar o contexto JS - persista logs de forma síncronaRelacionado: Manipuladores de Erros Globais - rejeições de promessas não tratadas e log de produção
Componha os primitivos desta página em um wrapper de tela reutilizável - o padrão no qual a maioria dos aplicativos de produção converge.
import React, { Component, useCallback, useEffect, useState, type ReactNode } from "react";
import {
ActivityIndicator,
Pressable,
Text,
View,
StyleSheet,
} from "react-native";
import NetInfo from "@react-native-community/netinfo";
class ScreenBoundary extends Component<
{ children: ReactNode; resetKey: number },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <Text style={styles.inlineError}>This screen crashed.</Text>;
}
return this.props.children;
}
}
function OfflineBanner() {
const [offline, setOffline] = useState(false);
useEffect(() => {
return NetInfo.addEventListener((state) => {
setOffline(!(state.isConnected && state.isInternetReachable !== false));
});
}, []);
if (!offline) return null;
return (
<View style={styles.banner}>
<Text style={styles.bannerText}>Offline</Text>
</View>
);
}
function OrdersScreen({ reloadKey }: { reloadKey: number }) {
const [status, setStatus] = useState<"loading" | "error" | "ready">("loading");
useEffect(() => {
let cancelled = false;
setStatus("loading");
fetch("https://jsonplaceholder.typicode.com/posts?_limit=2")
.then((res) => {
if (!res.ok) throw new Error("Could not load orders");
return res.json();
})
.then(() => {
if (!cancelled) setStatus("ready");
})
.catch(() => {
if (!cancelled) setStatus("error");
});
return () => {
cancelled = true;
};
}, [reloadKey]);
if (status === "loading") return <ActivityIndicator style={{ marginTop: 24 }} />;
if (status === "error") {
return <Text style={styles.inlineError}>Could not load orders.</Text>;
}
return <Text style={{ padding: 16 }}>Orders loaded.</Text>;
}
export default function App() {
const [screenKey, setScreenKey] = useState(0);
const [reloadKey, setReloadKey] = useState(0);
const retry = useCallback(() => {
setScreenKey((k) => k + 1);
setReloadKey((k) => k + 1);
}, []);
return (
<View style={styles.shell}>
<OfflineBanner />
<ScreenBoundary resetKey={screenKey}>
<OrdersScreen reloadKey={reloadKey} />
</ScreenBoundary>
<Pressable onPress={retry} style={styles.retry}>
<Text style={styles.retryText}>Retry screen</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
shell: { flex: 1, paddingTop: 48 },
banner: { backgroundColor: "#fef3c7", padding: 8 },
bannerText: { textAlign: "center", color: "#92400e", fontWeight: "600" },
inlineError: { padding: 16, color: "#b91c1c" },
retry: {
margin: 16,
backgroundColor: "#2563eb",
padding: 12,
borderRadius: 8,
alignItems: "center",
},
retryText: { color: "#fff", fontWeight: "600" },
});screenKey (remonta o boundary) quanto reloadKey (reexecuta o fetch)ScreenBoundary, OfflineBanner e ResilienceShell para components/ à medida que o aplicativo cresceRelacionado: Error Boundaries em RN - posicionamento do navegador | Feature Flags para Rollout Seguro - kill switches para telas travadas | Melhores Práticas de Error Boundaries - checklist de produção
Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026