Fundamentos del Manejo de Errores
10 ejemplos para comenzar con el Manejo de Errores - 7 básicos y 3 intermedios.
Busca en todas las páginas de la documentación
10 ejemplos para comenzar con el Manejo de Errores - 7 básicos y 3 intermedios.
Las apps móviles fallan de formas que las apps web rara vez lo hacen - LTE inestable, descargas en segundo plano y crashes de módulos nativos. Comienza desde un proyecto Expo TypeScript en blanco para que cada ejemplo a continuación pueda reemplazar App.tsx y ejecutarse inmediatamente.
npx create-expo-app@latest MyResilientApp --template blank-typescript
cd MyResilientApp
npx expo startLos ejemplos 8 y 10 utilizan detección de conectividad de red. Instala NetInfo una vez:
npx expo install @react-native-community/netinfoHerramientas: Estos ejemplos están dirigidos a Expo SDK 57, React Native 0.86 y React 19.2.3. TypeScript (
.tsx) se utiliza en todos.
Dos capas de error aparecen a lo largo de esta sección:
| Capa | Atrapa | Herramienta |
|---|---|---|
| Imperativa | fetch fallido, promesas rechazadas, JSON inválido, manejadores de eventos lanzados | try/catch + state |
| Declarativa | Crashes de renderizado, props inválidas, acceso undefined durante JSX | React Error Boundary |
Usa ambas. try/catch no puede capturar errores lanzados mientras React está renderizando; Error Boundaries no pueden capturar errores dentro de funciones async o manejadores onPress.
Envuelve las llamadas de red en try/catch para que un error lanzado se convierta en estado UI en lugar de un rechazo no manejado.
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(`La solicitud falló (${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 : "Algo salió mal");
} 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 pertenece al límite donde el trabajo asincrónico se encuentra con el state de React - típicamente dentro de useEffect o un manejador de eventosres.ok explícitamente; fetch solo rechaza en fallo de red, no en HTTP 4xx/5xxfinally garantiza que loading se limpie incluso cuando la solicitud lanzaerr instanceof Error antes de leer .message - los valores lanzados no siempre son objetos ErrorRelacionado: Network Failure UX - colas de reintento y stale-while-revalidate | User-Facing Error Copy - escribir mensajes en los que los usuarios puedan actuar
Los lanzamientos síncronos dentro de onPress evitan Error Boundaries. Atrápales 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("Ingresa un número positivo");
}
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 : "Entrada inválida");
}
}
return (
<View style={styles.container}>
<Text>Cantidad: {qty}</Text>
<Pressable onPress={() => setQty("0")} style={styles.chip}>
<Text>Establecer inválido (0)</Text>
</Pressable>
<Pressable onPress={handleCalculate} style={styles.button}>
<Text style={styles.buttonText}>Calcular 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" },
});"Ingresa un número positivo") sobre strings "Error" genéricosRelacionado: User-Facing Error Copy - mensajes accionables vs fallos opacos
Una unión de estado discriminada previene combinaciones de UI imposibles como mostrar un spinner y un banner de error simultáneamente.
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("No se pudo cargar el perfil");
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 : "Error desconocido",
}),
);
}, []);
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 TypeScript estreche status.data solo dentro de la rama "success"status reemplaza los booleanos paralelos loading, error y data que pueden desincronizarse.then/.catch es equivalente a try/catch dentro de un IIFE asincrónico - elige el que te parezca más claro en el efectoView en blanco es un modo de falloRelacionado: Network Failure UX - stale-while-revalidate cuando los errores son transitorios
Los Error Boundaries de React deben ser componentes de clase. Atrapan crashes en tiempo de renderizado en árboles secundarios y muestran 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" }}>Esta sección no se pudo renderizar.</Text>
</View>
);
}
return this.props.children;
}
}
function BrokenWidget() {
const config = undefined as unknown as { label: string };
return <Text>{config.label}</Text>; // lanza durante el renderizado
}
export default function App() {
return (
<ErrorBoundary>
<BrokenWidget />
</ErrorBoundary>
);
}getDerivedStateFromError cambia la UI al fallback; componentDidCatch es para logging y telemetríauseErrorBoundary - un pequeño envoltorio de clase sigue siendo el patrón idiomáticoinfo.componentStack en componentDidCatch; pinpointa qué componente de pantalla fallóRelacionado: Error Boundaries in RN - fallbacks a nivel de pantalla y UI de recuperación
Reinicia un boundary cambiando un key en el envoltorio para que React remonte el subárbol fallido.
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}>Algo salió mal</Text>
<Pressable onPress={this.props.onRetry} style={styles.button}>
<Text style={styles.buttonText}>Intentar de nuevo</Text>
</Pressable>
</View>
);
}
return this.props.children;
}
}
function FlakyChart({ shouldFail }: { shouldFail: boolean }) {
if (shouldFail) throw new Error("Chart render failed");
return <Text>Gráfico renderizado exitosamente</Text>;
}
export default function App() {
const [attempt, setAttempt] = useState(0);
const shouldFail = attempt < 2; // falla dos veces, éxito en el tercer intento
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 el boundary y limpia hasError - llamar a setState solo dentro del boundary no puede recuperarseattempt) para que tanto el boundary como el secundario se reinicien juntosonRetry como un prop para que el boundary siga siendo reutilizable en todas las pantallasRelacionado: Error Boundaries in RN - envolvimiento de navegadores y pantallas de tabulación
Un botón de reintento debe volver a invocar la función de fetch, no simplemente ocultar el texto de error.
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("Servicio de clima no disponible");
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 : "Solicitud fallida");
}
})
.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}>Reintentar</Text>
</Pressable>
</View>
);
}
return (
<View style={styles.center}>
<Text>Temperatura: {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 redetonador del efecto de forma limpia - evita duplicar la lógica de fetch dentro del manejador de reintentocancelled previene que respuestas obsoletas sobrescriban el state después de un doble tap rápido en Reintentarerror y establece loading al inicio de cada intento para que la UI muestre feedback de progresoloading si quieres prevenir solicitudes en vuelo duplicadasRelacionado: Network Failure UX - backoff exponencial y colas offline
Combina ambas capas: los boundaries contienen crashes de renderizado; try/catch maneja todo lo asincrónico.
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 }}>Pantalla crasheó - el boundary lo atrapó.</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 falló");
const users = await res.json();
setNames(users.map((u: { name: string }) => u.name));
} catch (err) {
setError(err instanceof Error ? err.message : "Carga fallida");
} 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 efectos y manejadores; Error Boundary para renderizado - la división es mecánica, no opcionalRelacionado: Error Boundaries in RN - ubicación alrededor de navegadores | Error Boundaries Best Practices - fallar contenido, registrar en voz alta
Surfea la pérdida de conectividad a nivel de shell para que cada pantalla herede el mismo 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}>Estás offline. Algunas acciones pueden no funcionar.</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>El contenido principal continúa debajo del 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 se dispara cuando el dispositivo se mueve entre Wi-Fi, LTE y modo aviónisConnected como isInternetReachable - los portales cautivos pueden reportar conectado pero inalcanzableapp/_layout.tsx en Expo Router) para que persista a través de la navegaciónRelacionado: Network Failure UX - colas offline y lecturas en caché | Graceful Degradation Patterns - modos reducidos sin conectividad
ErrorUtils.setGlobalHandler atrapa errores JS que escapan de cada boundary - registralo una vez al iniciar la app.
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) => {
// Envía a Sentry / Datadog / tu backend aquí
console.error("[global]", error.message, { isFatal });
// Preserva la redbox en desarrollo
defaultHandler(error, isFatal);
});
}
export default function App() {
useEffect(() => {
installGlobalHandler();
}, []);
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Manejador global registrado en montaje.</Text>
</View>
);
}setGlobalHandler una vez - típicamente en app/_layout.tsx o un bootstrap.ts dedicado importado antes de la navegacióndefaultHandler para que los desarrolladores aún vean la redbox en __DEV__isFatal === true significa que el runtime puede desgarrar el contexto de JS - persiste logs sincronícamenteRelacionado: Global Error Handlers - rechazos de promesas no manejados y logging de producción
Compone los primitivos de esta página en un envoltorio de pantalla reutilizable - el patrón en el que la mayoría de apps de producción convergen.
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}>Esta pantalla crasheó.</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("No se pudieron cargar los pedidos");
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}>No se pudieron cargar los pedidos.</Text>;
}
return <Text style={{ padding: 16 }}>Pedidos cargados.</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}>Reintentar pantalla</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 el boundary) como reloadKey (vuelve a ejecutar el fetch)ScreenBoundary, OfflineBanner y ResilienceShell en components/ conforme crece la appRelacionado: Error Boundaries in RN - ubicación de navegadores | Feature Flags for Safe Rollout - kill switches para pantallas que crashean | Error Boundaries Best Practices - checklist de producción
Versiones del Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026