Patrones de Manejo de Errores
Patrones consistentes y robustos para el manejo de errores en aplicaciones de producción.
Cuándo activarlo
- Diseñar tipos de error o jerarquías de excepción para un nuevo módulo o servicio
- Añadir lógica de reintento o interruptores de circuito para dependencias externas no confiables
- Revisar endpoints de API para detectar manejo de errores faltante
- Implementar mensajes de error y retroalimentación orientados al usuario
- Depurar fallas en cascada o errores silenciados inadvertidamente
Principios fundamentales
- Fallar rápido y con claridad: muestra los errores en el límite donde ocurren; no los ocultes
- Errores tipados sobre mensajes de texto: los errores son valores de primera clase con estructura
- Mensajes de usuario ≠ mensajes de desarrollador: muestra texto amigable a los usuarios, registra el contexto completo en el servidor
- Nunca silencies errores: cada bloque
catchdebe manejar, relanzar o registrar el error - Los errores son parte del contrato de tu API: documenta cada código de error que un cliente pueda recibir
TypeScript / JavaScript
Clases de Error Tipadas
// Define una jerarquía de errores para tu dominio
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly details?: unknown,
) {
super(message)
this.name = this.constructor.name
// Mantiene la cadena de prototipos correcta en JavaScript ES5 transpilado.
// Necesario para que las comprobaciones `instanceof` (por ejemplo, `error instanceof NotFoundError`)
// funcionen correctamente al extender la clase Error incorporada.
Object.setPrototypeOf(this, new.target.prototype)
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} no encontrado: ${id}`, 'NOT_FOUND', 404)
}
}
export class ValidationError extends AppError {
constructor(message: string, details: { field: string; message: string }[]) {
super(message, 'VALIDATION_ERROR', 422, details)
}
}
export class UnauthorizedError extends AppError {
constructor(reason = 'Autenticación requerida') {
super(reason, 'UNAUTHORIZED', 401)
}
}
export class RateLimitError extends AppError {
constructor(public readonly retryAfterMs: number) {
super('Límite de velocidad excedido', 'RATE_LIMITED', 429)
}
}
Patrón Result (estilo sin lanzar excepción)
Para operaciones donde el fallo es esperado y común (análisis sintáctico, llamadas externas):
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E }
function ok<T>(value: T): Result<T> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// Uso
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findUnique({ where: { id } })
if (!user) return err(new NotFoundError('Usuario', id))
return ok(user)
} catch (e) {
return err(new AppError('Error de base de datos', 'DB_ERROR'))
}
}
const result = await fetchUser('abc-123')
if (!result.ok) {
// TypeScript sabe que aquí result.error está disponible
logger.error('Error al obtener usuario', { error: result.error })
return
}
// TypeScript sabe que aquí result.value está disponible
console.log(result.value.email)
Manejador de Errores de API (Next.js / Express)
import { NextRequest, NextResponse } from 'next/server'
function handleApiError(error: unknown): NextResponse {
// Error de aplicación conocido
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
message: error.message,
...(error.details ? { details: error.details } : {}),
},
},
{ status: error.statusCode },
)
}
// Error de validación Zod
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Falló la validación de la solicitud',
details: error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
},
},
{ status: 422 },
)
}
// Error inesperado: registra detalles, devuelve mensaje genérico
console.error('Error inesperado:', error)
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'Ocurrió un error inesperado' } },
{ status: 500 },
)
}
export async function POST(req: NextRequest) {
try {
// ... lógica del manejador
} catch (error) {
return handleApiError(error)
}
}
Límite de Error de React
import { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
fallback: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
children: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info)
console.error('Error de React no manejado:', error, info)
}
render() {
if (this.state.hasError) return this.props.fallback
return this.props.children
}
}
// Uso
<ErrorBoundary fallback={<p>Algo salió mal. Por favor, actualiza la página.</p>}>
<MyComponent />
</ErrorBoundary>
Python
Jerarquía de Excepciones Personalizadas
class AppError(Exception):
"""Error base de la aplicación."""
def __init__(self, message: str, code: str, status_code: int = 500):
super().__init__(message)
self.code = code
self.status_code = status_code
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
super().__init__(f"{resource} no encontrado: {id}", "NOT_FOUND", 404)
class ValidationError(AppError):
def __init__(self, message: str, details: list[dict] | None = None):
super().__init__(message, "VALIDATION_ERROR", 422)
self.details = details or []
Manejador Global de Excepciones FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={"error": {"code": exc.code, "message": str(exc)}},
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
# Registra todos los detalles, devuelve mensaje genérico
logger.exception("Error inesperado", exc_info=exc)
return JSONResponse(
status_code=500,
content={"error": {"code": "INTERNAL_ERROR", "message": "Ocurrió un error inesperado"}},
)
Go
Errores Centinela y Envoltura de Errores
package domain
import "errors"
// Errores centinela para comprobación de tipos
var (
ErrNotFound = errors.New("no encontrado")
ErrUnauthorized = errors.New("no autorizado")
ErrConflict = errors.New("conflicto")
)
// Envuelve errores con contexto: nunca pierdas el original
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("usuario %s: %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("consultando usuario %s: %w", id, err)
}
return user, nil
}
// A nivel del manejador, desenvuelve para determinar la respuesta
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
if err != nil {
switch {
case errors.Is(err, domain.ErrNotFound):
writeError(w, http.StatusNotFound, "not_found", err.Error())
case errors.Is(err, domain.ErrUnauthorized):
writeError(w, http.StatusForbidden, "forbidden", "Acceso denegado")
default:
slog.Error("error inesperado", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Ocurrió un error inesperado")
}
return
}
writeJSON(w, http.StatusOK, user)
}
Reintento con Retroceso Exponencial
interface RetryOptions {
maxAttempts?: number
baseDelayMs?: number
maxDelayMs?: number
retryIf?: (error: unknown) => boolean
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 500,
maxDelayMs = 10_000,
retryIf = () => true,
} = options
let lastError: unknown
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
if (attempt === maxAttempts || !retryIf(error)) throw error
const jitter = Math.random() * baseDelayMs
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw lastError
}
// Uso: reintentar errores de red transitorios, no errores 4xx
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
maxAttempts: 3,
retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
})
Mensajes de Error Orientados al Usuario
Asigna códigos de error a mensajes legibles para humanos. Mantén los detalles técnicos fuera del texto visible para el usuario.
const USER_ERROR_MESSAGES: Record<string, string> = {
NOT_FOUND: 'El elemento solicitado no pudo ser encontrado.',
UNAUTHORIZED: 'Por favor, inicia sesión para continuar.',
FORBIDDEN: 'No tienes permiso para hacer esto.',
VALIDATION_ERROR: 'Por favor, verifica tu entrada e inténtalo de nuevo.',
RATE_LIMITED: 'Demasiadas solicitudes. Espera un momento e inténtalo de nuevo.',
INTERNAL_ERROR: 'Algo salió mal en nuestro sistema. Por favor, inténtalo de nuevo más tarde.',
}
export function getUserMessage(code: string): string {
return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
}
Lista de Verificación para el Manejo de Errores
Antes de fusionar cualquier código que toque el manejo de errores:
- Cada bloque
catchmaneja, relanza o registra el error: no hay silenciamiento inadvertido - Los errores de API siguen el formato estándar
{ error: { code, message } } - Los mensajes orientados al usuario no contienen trazas de pila ni detalles internos
- El contexto completo del error se registra en el servidor
- Las clases de error personalizadas extienden una base
AppErrorcon un campocode - Las funciones asíncronas muestran los errores a los llamadores: no hay "disparar y olvidar" sin un plan de respaldo
- La lógica de reintento solo reintenta errores recuperables (no errores de cliente 4xx)
- Los componentes React están envueltos en
ErrorBoundarypara errores de renderizado