Mensagens de Erro para o Usuário
Mensagens acionáveis vs. rastros de pilha opacos em dispositivos móveis.
Busque em todas as páginas da documentação
Mensagens acionáveis vs. rastros de pilha opacos em dispositivos móveis.
Cartão de receita de referência rápida - pronto para copiar e colar.
// src/errors/userMessages.ts
export type UserFacingError = {
title: string;
body: string;
action: "retry" | "settings" | "support" | "dismiss";
actionLabel: string;
};
const NETWORK: UserFacingError = {
title: "Você está offline",
body: "Verifique sua conexão e tente novamente. Suas alterações foram salvas neste dispositivo.",
action: "retry",
actionLabel: "Tentar novamente",
};
const UNKNOWN: UserFacingError = {
title: "Algo deu errado",
body: "Não foi possível concluir a ação. Tente novamente em um momento.",
action: "retry",
actionLabel: "Tentar novamente",
};
export function toUserFacingError(error: unknown): UserFacingError {
if (error instanceof TypeError && /network/i.test(error.message)) {
return NETWORK;
}
if (isHttpError(error, 401)) {
return {
title: "Sessão expirada",
body: "Faça login novamente para continuar.",
action: "dismiss",
actionLabel: "Fazer login",
};
}
if (isHttpError(error, 503)) {
return {
title: "Serviço ocupado",
body: "Nossos servidores estão temporariamente sobrecarregados. Aguarde alguns segundos e tente novamente.",
action: "retry",
actionLabel: "Tentar novamente",
};
}
return UNKNOWN;
}
function isHttpError(error: unknown, status: number): boolean {
return (
typeof error === "object" &&
error !== null &&
"status" in error &&
(error as { status: number }).status === status
);
}// src/components/ErrorCallout.tsx
import { Button, StyleSheet, Text, View } from "react-native";
import type { UserFacingError } from "@/errors/userMessages";
type Props = {
error: UserFacingError;
onAction: () => void;
};
export function ErrorCallout({ error, onAction }: Props) {
return (
<View
style={styles.box}
accessibilityRole="alert"
accessibilityLiveRegion="polite"
>
<Text style={styles.title}>{error.title}</Text>
<Text style={styles.body}>{error.body}</Text>
<Button title={error.actionLabel} onPress={onAction} />
</View>
);
}
const styles = StyleSheet.create({
box: {
padding: 16,
borderRadius: 8,
backgroundColor: "#fef2f2",
borderWidth: StyleSheet.hairlineWidth,
borderColor: "#fecaca",
gap: 8,
},
title: { fontSize: 16, fontWeight: "700", color: "#991b1b" },
body: { color: "#7f1d1d", lineHeight: 20 },
});Quando usar isso:
catch, fallback de ErrorBoundary ou fetch falho que, de outra forma, mostraria error.message ou um redbox.// src/errors/reportError.ts
import type { UserFacingError } from "./userMessages";
export function reportError(
scope: string,
cause: unknown,
user: UserFacingError,
): void {
if (__DEV__) {
console.group(`[${scope}]`);
console.error(cause);
console.log("User copy:", user);
console.groupEnd();
return;
}
// Produção: Sentry/Crashlytics com escopo + causa; nunca anexe o título do usuário a PII de análise
}
// src/screens/ProfileScreen.tsx
import { useCallback, useEffect, useState } from "react";
import {
ActivityIndicator,
Button,
StyleSheet,
Text,
View,
} from "react-native";
import { ErrorCallout } from "@/components/ErrorCallout";
import { reportError } from "@/errors/reportError";
import { toUserFacingError } from "@/errors/userMessages";
async function fetchProfile(): Promise<{ name: string }> {
const response = await fetch("https://api.example.com/me");
if (!response.ok) {
throw { status: response.status, message: await response.text() };
}
return (await response.json()) as { name: string };
}
export function ProfileScreen() {
const [name, setName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [userError, setUserError] = useState<ReturnType<
typeof toUserFacingError
> | null>(null);
const load = useCallback(async () => {
setLoading(true);
setUserError(null);
try {
const data = await fetchProfile();
setName(data.name);
} catch (cause) {
const user = toUserFacingError(cause);
setUserError(user);
reportError("ProfileScreen.load", cause, user);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
if (loading) {
return <ActivityIndicator accessibilityLabel="Carregando perfil" />;
}
if (userError) {
return (
<View style={styles.screen}>
<ErrorCallout
error={userError}
onAction={() => {
if (userError.action === "retry") void load();
// conectar login / suporte / configurações por enum de ação
}}
/>
</View>
);
}
return (
<View style={styles.screen}>
<Text style={styles.title}>Olá, {name}</Text>
<Button title="Atualizar" onPress={() => void load()} />
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24, justifyContent: "center", gap: 12 },
title: { fontSize: 22, fontWeight: "700" },
});
// src/components/ScreenErrorBoundary.tsx - a cópia do boundary corresponde ao mesmo tom
import { Component, type ReactNode } from "react";
import { Button, StyleSheet, Text, View } from "react-native";
type Props = { children: ReactNode };
type State = { hasError: boolean };
export class ScreenErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error) {
if (__DEV__) console.error(error);
// reportar ao serviço de crash em produção
}
render() {
if (this.state.hasError) {
return (
<View style={boundaryStyles.wrap} accessibilityRole="alert">
<Text style={boundaryStyles.title}>Esta tela encontrou um problema</Text>
<Text style={boundaryStyles.body}>
O restante do aplicativo ainda funciona. Tente novamente ou volte.
</Text>
<Button
title="Tentar novamente"
onPress={() => this.setState({ hasError: false })}
/>
</View>
);
}
return this.props.children;
}
}
const boundaryStyles = StyleSheet.create({
wrap: { flex: 1, justifyContent: "center", padding: 24, gap: 12 },
title: { fontSize: 18, fontWeight: "700" },
body: { color: "#64748b", lineHeight: 22 },
});O que isso demonstra:
toUserFacingError mapeia internos confusos para um vocabulário fixo de títulos, corpos e ações.reportError registra detalhes completos apenas em __DEV__ - usuários em produção nunca veem rastros de pilha.ErrorCallout usa accessibilityRole="alert" e accessibilityLiveRegion para VoiceOver/TalkBack.ErrorBoundary corresponde à cópia de erro de fetch - mesmo tom calmo, mesmo padrão "Tentar novamente".toUserFacingError(cause) uma vez; elas nunca ramificam em error.message no JSX.title, body, action, actionLabel controlam a UI e a análise sem vazar internos.cause bruta; a UI recebe apenas UserFacingError.| Parte | Diretriz | Exemplo |
|---|---|---|
| Título | 3–6 palavras, sem jargão | "Você está offline" |
| Corpo | O que aconteceu + o que está seguro + dica | "Verifique sua conexão. Seu rascunho está salvo." |
| Ação | Um verbo, corresponde à recuperação | "Tentar novamente" |
| Evitar | Códigos de status, nomes de exceção, URLs | Não "HTTP 503" ou "TypeError: undefined" |
| Categoria | Título do usuário | Ação |
|---|---|---|
| Offline / timeout | Você está offline | Tentar novamente |
| Sessão expirada | Sessão expirada | Fazer login |
| Servidor 5xx | Serviço ocupado | Tentar novamente |
| Permissão negada | Permissão necessária | Abrir configurações |
| Validação | Verifique os campos destacados | Dispensar |
| Desconhecido | Algo deu errado | Tentar novamente ou suporte |
export function DevDiagnostics({ error }: { error: unknown }) {
if (!__DEV__) return null;
return (
<Text style={{ fontFamily: "monospace", fontSize: 11 }}>
{error instanceof Error ? error.stack : String(error)}
</Text>
);
}Renderize DevDiagnostics abaixo de ErrorCallout apenas em builds de desenvolvimento. Remova-o de binários de lançamento - __DEV__ é falso em produção.
// Mantenha o tratamento de ação exaustivo
function handleErrorAction(
user: UserFacingError,
handlers: Record<UserFacingError["action"], () => void>,
) {
handlers[user.action]();
}error.message do fetch - Frequentemente "Network request failed" ou HTML bruto. Correção: Mapeie para UserFacingError; registre o corpo bruto apenas em reportError.Alert.alert - Hábito comum de depuração deixado em produção. Correção: Alert recebe apenas user.title e user.body.Button principal; suporte como link de texto terciário.userError ou um erro de campo inline.accessibilityLiveRegion="assertive" em cada validação de tecla interrompe leitores de tela. Correção: polite para erros de formulário; assertive apenas para falhas críticas em nível de tela.mappers bloqueia i18n. Correção: Retorne chaves de mensagem (errors.offline.title) e resolva com i18n.t no componente.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Mapper centralizado (esta página) | A maioria dos erros do aplicativo compartilha um pequeno vocabulário | Erros altamente específicos do domínio que necessitam de UX única por tela |
| Apenas toasts de erro | Falhas de sincronização em segundo plano não bloqueantes | Erros bloqueantes - toasts desaparecem antes que os usuários os leiam |
Campo message bruto da API | O backend gerencia cópias confiáveis e testadas pelo usuário de ponta a ponta | APIs de terceiros ou mensagens verbosas para desenvolvedores |
| Códigos de erro na UI ("E1042") | Aplicações B2B com suporte e agentes treinados | Aplicativos de consumo - códigos parecem culpa |
| Redbox / LogBox em produção | Nunca | Apenas em desenvolvimento - redboxes não são cópias voltadas para o usuário |
Não em produção. Em __DEV__, mostre pilhas abaixo do ErrorCallout amigável para engenheiros. A equipe de suporte pode procurar incidentes por timestamp/ID de usuário em seu painel - os usuários não precisam de um código de falha na tela.
Uma ou duas frases curtas (aproximadamente 120 caracteres). Telas de celular são estreitas; longas desculpas empurram o botão de ação para fora da tela.
Calmo, direto e prestativo. Reconheça o problema, declare o que está preservado ("seu rascunho está salvo") e dê um próximo passo. Evite humor para erros de pagamento, saúde ou autenticação.
Error Boundaries capturam erros de renderização - a cópia deve tranquilizar que o restante do aplicativo funciona e oferecer "Tentar novamente" ou voltar. Erros de fetch são falhas de operação - a cópia pode ser específica (offline, sessão expirada).
Deixe o usuário tocar em tentar novamente - loops de tentativa automática frustram usuários em modo avião e desperdiçam bateria. Exceção: sincronização em segundo plano com backoff exponencial, invisível para o usuário.
Inline abaixo do campo para validação ("Insira um e-mail válido"). ErrorCallout em nível de tela para falhas de envio (servidor rejeitou o formulário). Não duplique a mesma mensagem em ambos os lugares.
Armazene chaves de tradução no mapper, não strings literais, antes que o aplicativo envie vários locais. Mantenha strings em inglês como padrão para aplicativos em estágio inicial.
Reserve o suporte para falhas UNKNOWN ou repetidas após tentar novamente. Erros de offline e sessão expirada são corrigíveis pelo usuário - links de suporte adicionam ruído.
A UX de falha de rede abrange banners, filas e stale-while-revalidate. A cópia voltada para o usuário é a camada de redação dentro desses padrões - o mesmo tom, seja em um banner ou em um erro de tela inteira.
Somente se você controlar a API e testar as strings. Envolva até mesmo mensagens confiáveis em um mapa de lista de permissões - implantações inesperadas podem vazar fragmentos de SQL ou nomes de serviços internos.
accessibilityRole="alert", accessibilityLiveRegion="polite" (ou assertive para bloqueadores críticos) e uma ordem de foco visível que aterre no botão de ação após o anúncio.
ErrorUtils.setGlobalHandler captura falhas nativas/JS não tratadas - combine com uma tela genérica "Algo deu errado" e um prompt de reinicialização. Veja Manipuladores de Erro Globais para conexão; esta página é dona da cópia.
Error Boundaries falharemErrorBoundaryVersões de 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