Aprenda tratamento de erros em TypeScript. Use segurança de tipo, erros personalizados, type guards e mônadas de resultado para construir código robusto, previsível e mantenível.
Tratamento de Erros em TypeScript: Padrões de Segurança de Tipo para Exceções
No mundo do desenvolvimento de software, onde as aplicações impulsionam tudo, desde sistemas financeiros globais até interações móveis diárias, construir sistemas resilientes e tolerantes a falhas não é apenas uma boa prática — é uma necessidade fundamental. Embora o JavaScript ofereça um ambiente dinâmico e flexível, sua tipagem flexível pode, às vezes, levar a surpresas em tempo de execução, especialmente ao lidar com erros. É aqui que o TypeScript entra, trazendo a verificação estática de tipos para o primeiro plano e oferecendo ferramentas poderosas para aprimorar a previsibilidade e a manutenibilidade do código.
O tratamento de erros é um aspecto crítico de qualquer aplicação robusta. Sem uma estratégia clara, problemas inesperados podem levar a um comportamento imprevisível, corrupção de dados ou até mesmo falha completa do sistema. Quando combinado com a segurança de tipos do TypeScript, o tratamento de erros se transforma de uma tarefa de codificação defensiva em uma parte estruturada, previsível e gerenciável da arquitetura da sua aplicação.
Este guia abrangente aprofunda-se nas nuances do tratamento de erros em TypeScript, explorando vários padrões e melhores práticas para garantir a segurança de tipo das exceções. Iremos além do bloco básico try...catch, descobrindo como alavancar os recursos do TypeScript para definir, capturar e tratar erros com precisão incomparável. Quer esteja a construir uma aplicação empresarial complexa, um serviço web de alto tráfego ou uma experiência frontend de ponta, a compreensão destes padrões irá capacitá-lo a escrever código mais fiável, depurável e mantenível para uma audiência global de programadores e utilizadores.
A Fundação: O Objeto Error do JavaScript e try...catch
Antes de explorarmos os aprimoramentos do TypeScript, é essencial entender a base do tratamento de erros em JavaScript. O mecanismo central é o objeto Error, que serve como base para todos os erros padrão incorporados.
Tipos de Erro Padrão em JavaScript
Error: O objeto de erro base genérico. A maioria dos erros personalizados estende este.TypeError: Indica que uma operação foi realizada em um valor do tipo errado.ReferenceError: Lançado quando uma referência inválida é feita (ex: tentar usar uma variável não declarada).RangeError: Indica que uma variável numérica ou parâmetro está fora de seu intervalo válido.SyntaxError: Ocorre ao analisar código que não é JavaScript válido.URIError: Lançado quando funções comoencodeURI()oudecodeURI()são usadas de forma inadequada.EvalError: Relaciona-se com a função globaleval()(menos comum em código moderno).
Blocos Básicos try...catch
A maneira fundamental de tratar erros síncronos em JavaScript (e TypeScript) é com a declaração try...catch:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(`Result: ${result}`);
} catch (error) {
console.error("An error occurred:", error);
}
// Output:
// An error occurred: Error: Division by zero is not allowed.
No JavaScript tradicional, o parâmetro do bloco catch tinha implicitamente um tipo any. Isso significava que você poderia tratar error como qualquer coisa, levando a potenciais problemas em tempo de execução se você esperasse um formato de erro específico, mas recebesse outra coisa (por exemplo, uma simples string ou um número sendo lançado). Essa falta de segurança de tipo poderia tornar o tratamento de erros frágil e difícil de depurar.
Evolução do TypeScript: O Tipo unknown em Cláusulas Catch
Com a introdução do TypeScript 4.4, o tipo da variável da cláusula catch foi alterado de any para unknown. Esta foi uma melhoria significativa para a segurança de tipo. O tipo unknown força os programadores a restringir explicitamente o tipo do erro antes de operar sobre ele. Isso significa que você não pode simplesmente aceder a propriedades como error.message ou error.statusCode sem primeiro afirmar ou verificar o tipo de error. Esta alteração reflete um compromisso com garantias de tipo mais fortes, prevenindo armadilhas comuns onde os programadores assumem incorretamente a forma de um erro.
try {
throw "Oops, something went wrong!"; // Lançando uma string, o que é válido em JS
} catch (error) {
// Em TS 4.4+, 'error' é do tipo 'unknown'
// console.log(error.message); // ERRO: 'error' é do tipo 'unknown'.
}
Esta rigorosidade é uma funcionalidade, não um bug. Ela nos obriga a escrever uma lógica de tratamento de erros mais robusta, estabelecendo as bases para os padrões de segurança de tipo que exploraremos a seguir.
Por Que a Segurança de Tipo em Erros é Crucial para Aplicações Globais
Para aplicações que atendem uma base de usuários global e são desenvolvidas por equipes internacionais, o tratamento de erros consistente e previsível é primordial. A segurança de tipo em erros oferece várias vantagens distintas:
- Confiabilidade e Estabilidade Aprimoradas: Ao definir explicitamente os tipos de erro, você previne falhas inesperadas em tempo de execução que poderiam surgir ao tentar acessar propriedades inexistentes em um objeto de erro malformado. Isso leva a aplicações mais estáveis, críticas para serviços onde o tempo de inatividade pode ter custos financeiros ou de reputação significativos em diferentes mercados.
- Melhora da Experiência do Desenvolvedor (DX) e Manutenibilidade: Quando os desenvolvedores entendem claramente quais erros uma função pode lançar ou retornar, eles podem escrever uma lógica de tratamento mais direcionada e eficaz. Isso reduz a carga cognitiva, acelera o desenvolvimento e torna o código mais fácil de manter e refatorar, especialmente em equipes grandes e distribuídas que abrangem diferentes fusos horários e backgrounds culturais.
- Lógica de Tratamento de Erros Previsível: Erros com segurança de tipo permitem uma verificação exaustiva. Você pode escrever declarações
switchou cadeiasif/else ifque cobrem todos os tipos de erro possíveis, garantindo que nenhum erro passe despercebido. Essa previsibilidade é vital para sistemas que devem aderir a rigorosos acordos de nível de serviço (SLAs) ou padrões de conformidade regulatória em todo o mundo. - Melhor Depuração e Solução de Problemas: Tipos de erro específicos com metadados ricos fornecem contexto inestimável durante a depuração. Em vez de um genérico "algo deu errado", você obtém informações precisas como
NetworkErrorcom umstatusCode: 503, ouValidationErrorcom uma lista de campos inválidos. Essa clareza reduz drasticamente o tempo gasto no diagnóstico de problemas, um grande benefício para equipes de operações que trabalham em diversas localizações geográficas. - Contratos de API Claros: Ao projetar APIs ou módulos reutilizáveis, declarar explicitamente os tipos de erros que podem ser lançados torna-se parte do contrato da função. Isso melhora os pontos de integração, permitindo que outros serviços ou equipes interajam com seu código de forma mais previsível e segura.
- Facilita a Internacionalização de Mensagens de Erro: Com tipos de erro bem definidos, você pode mapear códigos de erro específicos para mensagens localizadas para usuários em diferentes idiomas e culturas. Um
UserNotFoundErrorpode apresentar "User not found" em inglês, "Utilisateur introuvable" em francês ou "Usuario no encontrado" em espanhol, aprimorando a experiência do usuário globalmente sem alterar a lógica subjacente de tratamento de erros.
Adotar a segurança de tipo no tratamento de erros é um investimento no futuro da sua aplicação, garantindo que ela permaneça robusta, escalável e gerenciável à medida que evolui e atende a uma audiência global.
Padrão 1: Verificação de Tipo em Tempo de Execução (Restrição de Erros unknown)
Dado que as variáveis do bloco catch são tipadas como unknown no TypeScript 4.4+, o primeiro e mais fundamental padrão é restringir o tipo do erro dentro do bloco catch. Isso garante que você esteja acessando apenas propriedades que são garantidamente existentes no objeto de erro após a verificação.
Usando instanceof Error
A maneira mais comum e direta de restringir um erro unknown é verificar se ele é uma instância da classe Error integrada (ou uma de suas classes derivadas como TypeError, ReferenceError, etc.).
function riskyOperation(): void {
// Simula diferentes tipos de erros
const rand = Math.random();
if (rand < 0.3) {
throw new Error("Generic error occurred!");
} else if (rand < 0.6) {
throw new TypeError("Invalid data type provided.");
} else {
throw { code: 500, message: "Internal Server Error" }; // Objeto não-Error
}
}
try {
riskyOperation();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`Caught an Error object: ${error.message}`);
// Você também pode verificar subclasses de Error específicas
if (error instanceof TypeError) {
console.error("Specifically, a TypeError was caught.");
}
} else if (typeof error === 'string') {
console.error(`Caught a string error: ${error}`);
} else if (typeof error === 'object' && error !== null && 'message' in error) {
// Trata objetos personalizados que possuem uma propriedade 'message'
console.error(`Caught a custom error object with message: ${(error as { message: string }).message}`);
} else {
console.error("An unexpected type of error occurred:", error);
}
}
Esta abordagem oferece segurança de tipo básica, permitindo que você acesse as propriedades message e name de objetos Error padrão. No entanto, para cenários de erro mais específicos, você desejará informações mais ricas.
Type Guards Personalizados para Objetos de Erro Específicos
Frequentemente, sua aplicação definirá suas próprias estruturas de erro personalizadas, talvez contendo códigos de erro específicos, identificadores únicos ou metadados adicionais. Para acessar com segurança essas propriedades personalizadas, você pode criar type guards definidos pelo usuário.
// 1. Defina interfaces/tipos de erro personalizados
interface NetworkError {
name: "NetworkError";
message: string;
statusCode: number;
url: string;
}
interface ValidationError {
name: "ValidationError";
message: string;
fields: { [key: string]: string };
}
// 2. Crie type guards para cada erro personalizado
function isNetworkError(error: unknown): error is NetworkError {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name: string }).name === "NetworkError" &&
'message' in error &&
'statusCode' in error &&
'url' in error
);
}
function isValidationError(error: unknown): error is ValidationError {
return (
typeof error === 'object' &&
error !== null &&
'name' in error &&
(error as { name: string }).name === "ValidationError" &&
'message' in error &&
'fields' in error &&
typeof (error as { fields: unknown }).fields === 'object'
);
}
// 3. Exemplo de uso em um 'try...catch'
function fetchData(url: string): Promise<any> {
return new Promise((resolve, reject) => {
// Simula uma chamada de API que pode lançar diferentes erros
const rand = Math.random();
if (rand < 0.4) {
reject(new Error("Something unexpected happened."));
} else if (rand < 0.7) {
reject({
name: "NetworkError",
message: "Failed to fetch data",
statusCode: 503,
url
} as NetworkError);
} else {
reject({
name: "ValidationError",
message: "Invalid input data",
fields: { 'email': 'Invalid format' }
} as ValidationError);
}
});
}
async function processData() {
const url = "https://api.example.com/data";
try {
const data = await fetchData(url);
console.log("Data fetched successfully:", data);
} catch (error: unknown) {
if (isNetworkError(error)) {
console.error(`Network Error from ${error.url}: ${error.message} (Status: ${error.statusCode})`);
// Tratamento específico para problemas de rede, ex: lógica de nova tentativa ou notificação ao usuário
} else if (isValidationError(error)) {
console.error(`Validation Error: ${error.message}`);
console.error("Invalid fields:", error.fields);
// Tratamento específico para erros de validação, ex: exibir erros ao lado dos campos do formulário
} else if (error instanceof Error) {
console.error(`Standard Error: ${error.message}`);
} else {
console.error("An unknown or unexpected error type occurred:", error);
// Fallback para erros verdadeiramente inesperados
}
}
}
processData();
Este padrão torna sua lógica de tratamento de erros significativamente mais robusta e legível. Ele força você a considerar e tratar explicitamente diferentes cenários de erro, o que é crucial para a construção de aplicações manteníveis.
Padrão 2: Classes de Erro Personalizadas
Embora os type guards em interfaces sejam úteis, uma abordagem mais estruturada e orientada a objetos é definir classes de erro personalizadas. Este padrão permite alavancar a herança, criando uma hierarquia de tipos de erro específicos que podem ser capturados e tratados com precisão usando verificações instanceof, semelhante aos erros JavaScript integrados, mas com suas próprias propriedades personalizadas.
Estendendo a Classe Error Integrada
A melhor prática para erros personalizados em TypeScript (e JavaScript) é estender a classe base Error. Isso garante que seus erros personalizados retenham propriedades como message e stack, que são vitais para depuração e registro.
// Erro Personalizado Base
class CustomApplicationError extends Error {
constructor(message: string, public code: string = 'GENERIC_ERROR') {
super(message);
this.name = this.constructor.name; // Define o nome do erro como o nome da classe
// Preserva o stack trace para melhor depuração
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Erros Personalizados Específicos
class DatabaseConnectionError extends CustomApplicationError {
constructor(message: string, public databaseName: string, public connectionString?: string) {
super(message, 'DB_CONN_ERROR');
}
}
class UserAuthenticationError extends CustomApplicationError {
constructor(message: string, public userId?: string, public reason: 'INVALID_CREDENTIALS' | 'SESSION_EXPIRED' | 'FORBIDDEN' = 'INVALID_CREDENTIALS') {
super(message, 'AUTH_ERROR');
}
}
class DataValidationFailedError extends CustomApplicationError {
constructor(message: string, public invalidFields: { [key: string]: string }) {
super(message, 'VALIDATION_ERROR');
}
}
Benefícios das Classes de Erro Personalizadas
- Significado Semântico: Os nomes das classes de erro fornecem uma visão imediata da natureza do problema (ex:
DatabaseConnectionErrorindica claramente um problema de banco de dados). - Extensibilidade: Você pode adicionar propriedades específicas a cada tipo de erro (ex:
statusCode,userId,fields) que são relevantes para aquele contexto de erro particular, enriquecendo as informações de erro para depuração e tratamento. - Fácil Identificação com
instanceof: Capturar e distinguir entre diferentes erros personalizados torna-se trivial usandoinstanceof, permitindo uma lógica de tratamento de erros precisa. - Manutenibilidade: Centralizar as definições de erro torna sua base de código mais fácil de entender e gerenciar. Se as propriedades de um erro mudarem, você atualiza uma definição de classe.
- Suporte de Ferramentas: IDEs e linters podem frequentemente fornecer melhores sugestões e avisos ao lidar com classes de erro distintas.
Tratando Classes de Erro Personalizadas
function performDatabaseOperation(query: string): any {
const rand = Math.random();
if (rand < 0.4) {
throw new DatabaseConnectionError("Failed to connect to primary DB", "users_db");
} else if (rand < 0.7) {
throw new UserAuthenticationError("User session expired", "user123", 'SESSION_EXPIRED');
} else {
throw new DataValidationFailedError("User input invalid", { 'name': 'Name is too short', 'email': 'Invalid email format' });
}
}
try {
performDatabaseOperation("SELECT * FROM users");
} catch (error: unknown) {
if (error instanceof DatabaseConnectionError) {
console.error(`Database Error: ${error.message}. DB: ${error.databaseName}. Code: ${error.code}`);
// Lógica para tentar reconectar ou notificar a equipe de operações
} else if (error instanceof UserAuthenticationError) {
console.warn(`Authentication Error (${error.reason}): ${error.message}. User ID: ${error.userId || 'N/A'}`);
// Lógica para redirecionar para a página de login ou atualizar token
} else if (error instanceof DataValidationFailedError) {
console.error(`Validation Error: ${error.message}. Invalid fields: ${JSON.stringify(error.invalidFields)}`);
// Lógica para exibir mensagens de validação ao usuário
} else if (error instanceof Error) {
console.error(`An unexpected standard error occurred: ${error.message}`);
} else {
console.error("A truly unexpected error occurred:", error);
}
}
Usar classes de erro personalizadas eleva significativamente a qualidade do seu tratamento de erros. Isso permite que você construa sistemas sofisticados de gerenciamento de erros que são robustos e fáceis de entender, o que é especialmente valioso para aplicações de larga escala com lógica de negócios complexa.
Padrão 3: O Padrão Mônada Result/Either (Tratamento Explícito de Erros)
Embora o try...catch com classes de erro personalizadas forneça um tratamento robusto para exceções, alguns paradigmas de programação funcional argumentam que as exceções quebram o fluxo normal de controle e podem tornar o código mais difícil de raciocinar, especialmente ao lidar com operações assíncronas. O padrão mônada "Result" ou "Either" oferece uma alternativa ao tornar o sucesso e a falha explícitos no tipo de retorno de uma função, forçando o chamador a lidar com ambos os resultados sem depender de try/catch para o fluxo de controle.
O Que é o Padrão Result/Either?
Em vez de lançar um erro, uma função que pode falhar retorna um tipo especial (muitas vezes chamado Result ou Either) que encapsula um valor bem-sucedido (Ok ou Right) ou um erro (Err ou Left). Este padrão é comum em linguagens como Rust (Result<T, E>) e Scala (Either<L, R>).
A ideia central é que o próprio tipo de retorno informa que a função tem dois resultados possíveis, e o sistema de tipos do TypeScript garante que você lide com ambos.
Implementando um Tipo Result Simples
type Result<T, E> = { success: true; value: T } | { success: false; error: E };
// Funções auxiliares para criar resultados Ok e Err
const ok = <T, E>(value: T): Result<T, E> => ({ success: true, value });
const err = <T, E>(error: E): Result<T, E> => ({ success: false, error });
interface User {
id: string;
name: string;
email: string;
}
// Erros personalizados para este padrão (ainda podem usar classes)
class UserNotFoundError extends Error {
constructor(userId: string) {
super(`User with ID '${userId}' not found.`);
this.name = 'UserNotFoundError';
}
}
class DatabaseReadError extends Error {
constructor(message: string, public details?: string) {
super(message);
this.name = 'DatabaseReadError';
}
}
// Função que retorna um tipo Result
function getUserById(id: string): Result<User, UserNotFoundError | DatabaseReadError> {
// Simula operação de banco de dados
const rand = Math.random();
if (rand < 0.3) {
return err(new UserNotFoundError(id)); // Retorna um resultado de erro
} else if (rand < 0.6) {
return err(new DatabaseReadError("Failed to read from DB", "Connection timed out")); // Retorna um erro de banco de dados
} else {
return ok({
id: id,
name: "John Doe",
email: `john.${id}@example.com`
}); // Retorna um resultado de sucesso
}
}
// Consumindo o tipo Result
const userResult = getUserById("user-123");
if (userResult.success) {
console.log(`User found: ${userResult.value.name}, Email: ${userResult.value.email}`);
} else {
// TypeScript sabe que userResult.error é do tipo UserNotFoundError | DatabaseReadError
if (userResult.error instanceof UserNotFoundError) {
console.error(`Application Error: ${userResult.error.message}`);
// Lógica para usuário não encontrado, ex: exibir uma mensagem ao usuário
} else if (userResult.error instanceof DatabaseReadError) {
console.error(`System Error: ${userResult.error.message}. Details: ${userResult.error.details}`);
// Lógica para problema de banco de dados, ex: tentar novamente ou alertar administradores do sistema
} else {
// Verificação exaustiva ou fallback para outros erros potenciais
console.error("An unexpected error occurred:", userResult.error);
}
}
Este padrão pode ser particularmente poderoso ao encadear operações que podem falhar, pois você pode usar map, flatMap (ou andThen) e outras construções funcionais para processar o Result sem verificações explícitas if/else em cada etapa, adiando o tratamento de erros para um único ponto.
Benefícios do Padrão Result
- Tratamento Explícito de Erros: As funções declaram explicitamente quais erros podem retornar em sua assinatura de tipo, forçando o chamador a reconhecer e lidar com todos os possíveis estados de falha. Isso elimina exceções "esquecidas".
- Transparência Referencial: Ao evitar exceções como um mecanismo de fluxo de controle, as funções tornam-se mais previsíveis e fáceis de testar.
- Legibilidade Aprimorada: O caminho do código para sucesso e falha é claramente delineado, tornando mais fácil seguir a lógica.
- Composicionalidade: Os tipos Result se compõem bem com técnicas de programação funcional, permitindo uma propagação e transformação elegante de erros.
- Sem Boilerplate
try...catch: Em muitos cenários, este padrão pode reduzir a necessidade de blocostry...catch, especialmente ao compor múltiplas operações passíveis de falha.
Considerações e Compromissos
- Verbosiade: Pode ser mais verboso para operações simples ou quando não se alavanca efetivamente as construções funcionais.
- Curva de Aprendizagem: Desenvolvedores novos em programação funcional ou mônadas podem achar este padrão inicialmente complexo.
- Operações Assíncronas: Embora aplicável, a integração com código assíncrono baseado em Promise existente requer um empacotamento ou transformação cuidadosa. Bibliotecas como
neverthrowoufp-tsfornecem implementações mais sofisticadas deEither/Resultadaptadas para TypeScript, frequentemente com melhor suporte assíncrono.
O padrão Result/Either é uma excelente escolha para aplicações que priorizam o tratamento explícito de erros, a pureza funcional e uma forte ênfase na segurança de tipo em todos os caminhos de execução. É particularmente adequado para sistemas de missão crítica onde cada modo de falha potencial deve ser explicitamente considerado.
Padrão 4: Estratégias Centralizadas de Tratamento de Erros
Enquanto blocos try...catch individuais e tipos Result lidam com erros locais, aplicações maiores, especialmente aquelas que atendem uma base de usuários global, beneficiam-se imensamente de estratégias centralizadas de tratamento de erros. Essas estratégias garantem relatórios de erro, registro e feedback do usuário consistentes em todo o sistema, independentemente de onde um erro se originou.
Manipuladores de Erro Globais
Centralizar o tratamento de erros permite que você:
- Registre erros consistentemente em um sistema de monitoramento (ex: Sentry, Datadog).
- Forneça mensagens de erro genéricas e amigáveis ao usuário para erros desconhecidos.
- Lide com preocupações em toda a aplicação, como envio de notificações, reversão de transações ou acionamento de disjuntores (circuit breakers).
- Garanta que PII (Informações de Identificação Pessoal) ou dados sensíveis não sejam expostos em mensagens de erro para usuários ou logs em violação de regulamentações de privacidade de dados (ex: GDPR, CCPA).
Exemplo de Backend (Node.js/Express)
Em uma aplicação Node.js Express, você pode definir um middleware de tratamento de erros que captura todos os erros lançados por suas rotas e outros middlewares. Este middleware deve ser o último a ser registrado.
import express, { Request, Response, NextFunction } from 'express';
// Assumimos que estas são nossas classes de erro personalizadas
class APIError extends Error {
constructor(message: string, public statusCode: number = 500) {
super(message);
this.name = 'APIError';
}
}
class UnauthorizedError extends APIError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
this.name = 'UnauthorizedError';
}
}
class BadRequestError extends APIError {
constructor(message: string = 'Bad Request') {
super(message, 400);
this.name = 'BadRequestError';
}
}
const app = express();
app.get('/api/users/:id', (req: Request, res: Response, next: NextFunction) => {
const userId = req.params.id;
if (userId === 'admin') {
return next(new UnauthorizedError('Access denied for admin user.'));
}
if (!/^[a-z0-9]+$/.test(userId)) {
return next(new BadRequestError('Invalid user ID format.'));
}
// Simula uma operação bem-sucedida ou outro erro inesperado
const rand = Math.random();
if (rand < 0.5) {
// Busca o usuário com sucesso
res.json({ id: userId, name: 'Test User' });
} else {
// Simula um erro interno inesperado
next(new Error('Failed to retrieve user data due to an unexpected issue.'));
}
});
// Middleware de tratamento de erros com segurança de tipo
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
// Registra o erro para monitoramento interno
console.error(`[ERROR] ${new Date().toISOString()} - ${req.method} ${req.originalUrl} -`, err);
if (err instanceof APIError) {
// Tratamento específico para erros de API conhecidos
return res.status(err.statusCode).json({
status: 'error',
message: err.message,
code: err.name // Ou um código de erro específico definido pela aplicação
});
} else if (err instanceof Error) {
// Tratamento genérico para erros padrão inesperados
return res.status(500).json({
status: 'error',
message: 'An unexpected server error occurred.',
// Em produção, evite expor mensagens de erro internas detalhadas aos clientes
detail: process.env.NODE_ENV === 'development' ? err.message : undefined
});
} else {
// Fallback para tipos de erro verdadeiramente desconhecidos
return res.status(500).json({
status: 'error',
message: 'An unknown server error occurred.',
detail: process.env.NODE_ENV === 'development' ? String(err) : undefined
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// Comandos cURL de exemplo:
// curl http://localhost:3000/api/users/admin
// curl http://localhost:3000/api/users/invalid-id!
// curl http://localhost:3000/api/users/valid-id
Exemplo de Frontend (React): Error Boundaries
Em frameworks de frontend como React, Error Boundaries fornecem uma maneira de capturar erros JavaScript em qualquer lugar de sua árvore de componentes filhos, registrar esses erros e exibir uma UI de fallback em vez de travar toda a aplicação. O TypeScript ajuda a definir as props e o estado para esses limites e a verificar o tipo do objeto de erro.
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode; // UI de fallback personalizada opcional
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class AppErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = {
hasError: false,
error: null,
errorInfo: null,
};
// Este método estático é chamado depois que um erro foi lançado por um componente descendente.
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return { hasError: true, error: _, errorInfo: null };
}
// Este método é chamado depois que um erro foi lançado por um componente descendente.
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Você também pode registrar o erro em um serviço de relatório de erros aqui
console.error("Uncaught error in AppErrorBoundary:", error, errorInfo);
this.setState({ errorInfo: errorInfo, error: error });
}
public render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: '20px', border: '1px solid red', borderRadius: '5px' }}>
<h2>Ops! Algo deu errado.</h2>
<p>Pedimos desculpa pelo inconveniente. Por favor, tente atualizar a página ou contacte o suporte.</p>
{this.state.error && (
<details style={{ whiteSpace: 'pre-wrap', color: '#666' }}>
<summary>Detalhes do Erro</summary>
<p>{this.state.error.message}</p>
{this.state.errorInfo && (
<p>Pilha de Componentes:<br/>{this.state.errorInfo.componentStack}</p>
)}
</details>
)}
</div>
);
}
return this.props.children;
}
}
// Como usar:
// function App() {
// return (
// <AppErrorBoundary>
// <SomePotentiallyFailingComponent />
// </AppErrorBoundary>
// );
// }
Distinguindo Erros Operacionais vs. Erros de Programador
Um aspecto crucial do tratamento de erros centralizado é distinguir entre duas categorias principais de erros:
- Erros Operacionais: São problemas previsíveis que podem ocorrer durante a operação normal, muitas vezes externos à lógica central da aplicação. Exemplos incluem tempos limite de rede, falhas de conexão com o banco de dados, entrada de usuário inválida, arquivo não encontrado ou limites de taxa. Esses erros devem ser tratados graciosamente, muitas vezes resultando em mensagens amigáveis ao usuário ou lógica de nova tentativa específica. Eles geralmente não indicam um bug em seu código. Classes de erro personalizadas com códigos de erro específicos são excelentes para isso.
- Erros de Programador: São bugs em seu código. Exemplos incluem
ReferenceError(usando uma variável indefinida),TypeError(chamando um método emnull) ou erros de lógica que levam a estados inesperados. Estes são geralmente irrecuperáveis em tempo de execução e exigem uma correção de código. Manipuladores de erro globais devem registrá-los extensivamente e potencialmente acionar reinicializações da aplicação ou alertas para a equipe de desenvolvimento.
Ao categorizar erros, seu manipulador centralizado pode decidir se exibe uma mensagem de erro genérica, tenta a recuperação ou escala o problema para os desenvolvedores. Essa distinção é vital para manter uma aplicação saudável e responsiva em diversos ambientes.
Melhores Práticas para o Tratamento de Erros com Segurança de Tipo
Para maximizar os benefícios do TypeScript em sua estratégia de tratamento de erros, considere estas melhores práticas:
- Sempre Restrinja
unknownem Blocoscatch: Desde o TypeScript 4.4+, a variávelcatchéunknown. Sempre realize verificações de tipo em tempo de execução (ex:instanceof Error, type guards personalizados) para acessar com segurança as propriedades de erro. Isso previne erros comuns em tempo de execução. - Projete Classes de Erro Personalizadas Significativas: Estenda a classe base
Errorpara criar tipos de erro específicos e semanticamente ricos. Inclua propriedades relevantes e contextuais (ex:statusCode,errorCode,invalidFields,userId) para auxiliar na depuração e tratamento. - Seja Explícito Sobre os Contratos de Erro: Documente os erros que uma função pode lançar ou retornar. Se usar o padrão Result, isso é imposto pela assinatura do tipo de retorno. Para
try/catch, comentários JSDoc claros ou assinaturas de função que transmitam exceções potenciais são valiosos. - Registre Erros de Forma Abrangente: Use uma abordagem de registro estruturada. Capture o stack trace completo do erro, juntamente com quaisquer propriedades de erro personalizadas e informações contextuais (ex: ID da requisição, ID do usuário, timestamp, ambiente). Para aplicações críticas, integre com um sistema centralizado de registro e monitoramento (ex: ELK Stack, Splunk, DataDog, Sentry).
- Evite Lançar Tipos Genéricos
stringouobject: Embora o JavaScript permita, lançar strings, números ou objetos simples brutos torna o tratamento de erros com segurança de tipo impossível e leva a código frágil. Sempre lance instâncias deErrorou classes de erro personalizadas. - Aproveite
neverpara Verificação Exaustiva: Ao lidar com uma união de tipos de erro personalizados (ex: em uma declaraçãoswitchou uma série deif/else if), use um type guard que leve a um tiponeverpara o blocoelsefinal. Isso garante que, se um novo tipo de erro for introduzido, o TypeScript sinalizará o caso não tratado. - Traduza Erros para a Experiência do Usuário: Mensagens de erro internas são para desenvolvedores. Para usuários finais, traduza erros técnicos em mensagens claras, acionáveis e culturalmente apropriadas. Considere usar códigos de erro que mapeiem para mensagens localizadas para suportar a internacionalização.
- Distinguir Entre Erros Recuperáveis e Irrecuperáveis: Projete sua lógica de tratamento de erros para diferenciar entre erros que podem ser repetidos ou autocorrigidos (ex: problemas de rede) e aqueles que indicam uma falha fatal na aplicação (ex: erros de programador não tratados).
- Teste Seus Caminhos de Erro: Assim como você testa os caminhos felizes, teste rigorosamente seus caminhos de erro. Garanta que sua aplicação lide graciosamente com todas as condições de erro esperadas e falhe de forma previsível quando ocorrerem inesperadas.
type SpecificError = DatabaseConnectionError | UserAuthenticationError | DataValidationFailedError;
function handleSpecificError(error: SpecificError) {
if (error instanceof DatabaseConnectionError) {
// ...
} else if (error instanceof UserAuthenticationError) {
// ...
} else if (error instanceof DataValidationFailedError) {
// ...
} else {
// Esta linha deve ser idealmente inatingível. Se for, um novo tipo de erro foi adicionado
// a SpecificError mas não tratado aqui, causando um erro TS.
const exhaustiveCheck: never = error; // O TypeScript sinalizará isso se 'error' não for 'never'
}
}
Aderir a essas práticas elevará suas aplicações TypeScript de meramente funcionais para robustas, confiáveis e altamente manteníveis, capazes de servir diversas bases de usuários em todo o mundo.
Conclusão
O tratamento eficaz de erros é um pilar fundamental do desenvolvimento de software profissional, e o TypeScript eleva esta disciplina crítica a novos patamares. Ao adotar padrões de tratamento de erros com segurança de tipo, os desenvolvedores podem ir além da correção reativa de bugs para um design de sistema proativo, construindo aplicações que são inerentemente mais resilientes, previsíveis e manteníveis.
Exploramos vários padrões poderosos:
- Verificação de Tipo em Tempo de Execução: Restringindo com segurança erros
unknownem blocoscatchusandoinstanceof Errore type guards personalizados para garantir acesso previsível às propriedades de erro. - Classes de Erro Personalizadas: Projetando uma hierarquia de tipos de erro semânticos que estendem a base
Error, fornecendo informações contextuais ricas e facilitando o tratamento preciso com verificaçõesinstanceof. - O Padrão Mônada Result/Either: Uma abordagem funcional alternativa que codifica explicitamente o sucesso e a falha nos tipos de retorno da função, compelindo os chamadores a lidar com ambos os resultados e reduzindo a dependência de mecanismos de exceção tradicionais.
- Tratamento Centralizado de Erros: Implementando manipuladores de erro globais (ex: middleware, error boundaries) para garantir registro, monitoramento e feedback do usuário consistentes em toda a aplicação, distinguindo entre erros operacionais e de programador.
Cada padrão oferece vantagens únicas, e a escolha ideal muitas vezes depende do contexto específico, estilo arquitetural e preferências da equipe. No entanto, o fio condutor comum em todas essas abordagens é o compromisso com a segurança de tipo. O sistema de tipos rigoroso do TypeScript atua como um poderoso guardião, guiando você em direção a contratos de erro mais robustos e ajudando a capturar problemas potenciais em tempo de compilação, em vez de em tempo de execução.
Adotar essas estratégias é um investimento que gera dividendos em estabilidade da aplicação, produtividade do desenvolvedor e satisfação geral do usuário, especialmente ao operar em um cenário de software global dinâmico e diversificado. Comece a integrar esses padrões de tratamento de erros com segurança de tipo em seus projetos TypeScript hoje mesmo e construa aplicações que permaneçam fortes contra os desafios inevitáveis do mundo digital.