El manejo de errores es un aspecto fundamental de la programación que asegura que las aplicaciones puedan manejar situaciones inesperadas de manera elegante. En JavaScript, aunque try-catch
se utiliza comúnmente, existen técnicas más avanzadas para mejorar el manejo de errores.
Este artículo explora estos métodos avanzados, proporcionando soluciones prácticas para mejorar tus estrategias de gestión de errores y hacer que tus aplicaciones sean más resilientes.
¿Qué es el manejo de errores?
El propósito del manejo de errores
El manejo de errores anticipa, detecta y responde a los problemas que surgen durante la ejecución del programa. Un manejo adecuado de errores mejora la experiencia del usuario, mantiene la estabilidad de la aplicación y asegura la fiabilidad.
Tipos de errores en JavaScript
- Errores de sintaxis. Estos son errores en la sintaxis del código, como corchetes faltantes o uso incorrecto de palabras clave.
- Errores en tiempo de ejecución. Ocurren durante la ejecución, como acceder a propiedades de objetos indefinidos.
- Errores lógicos. Estos errores no provocan que el programa se bloquee, pero conducen a resultados incorrectos, a menudo debido a una lógica defectuosa o efectos secundarios no deseados.
Por qué try-catch no es suficiente
Las limitaciones de try-catch
- Limitaciones de ámbito. Solo maneja código sincrónico dentro de su bloque y no afecta las operaciones asíncronas a menos que se manejen específicamente.
- Fallos silenciosos. El uso excesivo o el uso inadecuado pueden llevar a que los errores sean ignorados silenciosamente, lo que puede causar un comportamiento inesperado.
- Propagación de errores. No soporta de forma nativa la propagación de errores a través de las diferentes capas de la aplicación.
Cuándo usar try-catch
- Código síncrono. Efectivo para manejar errores en operaciones síncronas como el análisis de JSON.
- Secciones críticas. Usar para proteger secciones de código críticas donde los errores pueden tener consecuencias severas.
Clases de error personalizadas: Mejorando la información de error
Creando una clase de error personalizada
Las clases de error personalizadas extienden la clase Error
incorporada para proporcionar información adicional:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.stack = (new Error()).stack; // Capture the stack trace
}
}
Beneficios de los errores personalizados
- Claridad. Ofrece mensajes de error específicos.
- Manejo granular. Permite manejar tipos específicos de error por separado.
- Metadatos de error. Incluye contexto adicional sobre el error.
Casos de uso para errores personalizados
- Fallos de validación. Errores relacionados con la validación de la entrada del usuario.
- Errores específicos de dominio. Errores adaptados a dominios específicos de la aplicación como autenticación o procesamiento de pagos.
Manejo centralizado de errores
Manejo global de errores en Node.js
Centraliza el manejo de errores utilizando middleware:
app.use((err, req, res, next) => {
console.error('Global error handler:', err);
res.status(500).json({ message: 'An error occurred' });
});
Manejo centralizado de errores en aplicaciones frontend
Implementar un manejo de errores centralizado en React utilizando límites de error:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Ventajas del Manejo de Errores Centralizado
- Consistencia. Asegura un enfoque uniforme para la gestión de errores.
- Mantenimiento más fácil. Las actualizaciones centralizadas reducen el riesgo de omitir cambios.
- Mejor registro y monitoreo. Facilita la integración con herramientas de monitoreo.
Propagación de Errores
Propagación de Errores en Código Sincrónico
Usar throw
para propagar errores:
function processData(data) {
try {
validateData(data);
saveData(data);
} catch (error) {
console.error('Failed to process data:', error);
throw error;
}
}
Propagación de Errores en Código Asincrónico
Manejar errores con promesas o async
/await
:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
Cuándo Propagar Errores
- Errores críticos. Propagar errores que afectan a toda la aplicación.
- Logística de negocio. Permitir que los componentes de nivel superior manejen errores de lógica de negocio.
Manejo de Errores en Código Asincrónico
Manejo de Errores con async/await
Usar try-catch
para gestionar errores en funciones async:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
Uso de Promise.all con Manejo de Errores
Manejar múltiples promesas y errores:
async function fetchMultipleData(urls) {
try {
const responses = await Promise.all(urls.map
(url => fetch(url)));
return await Promise.all(responses.map(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${response.url}`);
}
return response.json();
}));
} catch (error) {
console.error('Error fetching multiple data:', error);
return [];
}
}
Trampas Comunes en el Manejo de Errores Asincrónicos
- Promesas no capturadas. Siempre manejar promesas usando
await
,.then()
, o.catch()
. - Fallos silenciosos. Asegurarse de que los errores no se ignoren silenciosamente.
- Condiciones de carrera. Tenga cuidado con las operaciones asincrónicas concurrentes.
Registro de errores
Registro de errores del lado del cliente
Capturar errores globales:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error captured:', message, source, lineno, colno, error);
sendErrorToService({ message, source, lineno, colno, error });
};
Registro de errores del lado del servidor
Utilice herramientas como Winston para el registro del lado del servidor:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log' })]
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
Monitoreo y alertas
Configure monitoreo y alertas en tiempo real con servicios como PagerDuty o Slack:
function notifyError(error) {
// Send error details to monitoring service
}
Mejores prácticas para el registro de errores
- Incluir contexto. Registre información adicional como datos de solicitud e información del usuario.
- Evitar el sobre-registro. Registre información esencial para prevenir problemas de rendimiento.
- Analizar registros regularmente. Revise los registros regularmente para detectar y abordar problemas recurrentes.
Degradación elegante y alternativas
Degradación elegante
Diseñe su aplicación para seguir funcionando con capacidades reducidas:
function renderProfilePicture(user) {
try {
if (!user.profilePicture) {
throw new Error('Profile picture not available');
}
return `<img data-fr-src="${user.profilePicture}" alt="Profile Picture">`;
} catch (error) {
console.error('Error rendering profile picture:', error.message);
return '<img src="/default-profile.png" alt="Default Profile Picture">';
}
}
Mecanismos de respaldo
Proporcione alternativas cuando las operaciones principales fallen:
async function fetchDataWithFallback(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
return { message: 'Default data' }; // Fallback data
}
}
Implementación de la degradación elegante
- Respaldo de UI. Proporcione elementos de UI alternativos cuando las funciones fallen.
- Respaldo de datos. Use valores en caché o predeterminados cuando los datos en vivo no estén disponibles.
- Mecanismos de reintento. Implemente lógica de reintento para errores transitorios.
Equilibrio de la degradación elegante
Equilibre la provisión de alternativas con mantener a los usuarios informados sobre los problemas:
function showErrorNotification(message) {
// Notify users about the issue
}
Pruebas de Manejo de Errores
Pruebas Unitarias de Manejo de Errores
Verificar el manejo de errores en funciones individuales:
const { validateUserInput } = require('./validation');
test('throws error for invalid username', () => {
expect(() => {
validateUserInput({ username: 'ab' });
}).toThrow('Username must be at least 3 characters long.');
});
Pruebas de Integración
Probar el manejo de errores en diferentes capas de la aplicación:
test('fetches data with fallback on error', async () => {
fetch.mockReject(new Error('Network error'));
const data = await fetchDataWithFallback('https://api.example.com/data');
expect(data).toEqual({ message: 'Default data' });
});
Pruebas de Extremo a Extremo
Simular escenarios del mundo real para probar el manejo de errores:
describe('ErrorBoundary', () => {
it('displays error message on error', () => {
cy.mount(<ErrorBoundary><MyComponent /></ErrorBoundary>);
cy.get(MyComponent).then(component => {
component.simulateError(new Error('Test error'));
});
cy.contains('Something went wrong.').should('be.visible');
});
});
Mejores Prácticas para las Pruebas de Manejo de Errores
- Cubrir casos límite. Asegurarse de que las pruebas aborden varios escenarios de error.
- Probar alternativas. Verificar que los mecanismos de alternativa funcionen según lo previsto.
- Automatizar las pruebas. Utilizar pipelines de CI/CD para automatizar y garantizar un manejo de errores robusto.
Escenarios del Mundo Real
Escenario 1: Sistema de Procesamiento de Pagos
Manejar errores durante el procesamiento de pagos:
- Clases de error personalizadas. Utilizar clases como
CardValidationError
,PaymentGatewayError
. - Logica de reintento. Implementar reintentos para problemas relacionados con la red.
- Registro centralizado. Monitorear errores de pago y abordar problemas de manera rápida.
Escenario 2: Aplicaciones Intensivas en Datos
Gestionar errores en el procesamiento de datos:
- Degradación elegante. Proporcionar datos parciales o vistas alternativas.
- Datos de respaldo. Utilice valores almacenados en caché o valores predeterminados.
- Registro de errores. Registre el contexto detallado para la solución de problemas.
Escenario 3: Autenticación y autorización de usuarios
Gestione errores de autenticación y autorización:
- Clases de error personalizadas. Cree clases como
ErrorDeAutenticación
,ErrorDeAutorización
. - Manejo centralizado. Registre y supervise problemas relacionados con la autenticación.
- Degradación elegante. Ofrezca opciones de inicio de sesión alternativas y mensajes de error significativos.
Conclusión
El manejo avanzado de errores en JavaScript requiere ir más allá del simple try-catch
para adoptar errores personalizados, manejo centralizado, propagación y pruebas robustas. Implementar estas técnicas le permite construir aplicaciones resilientes que brindan una experiencia de usuario fluida, incluso cuando las cosas salen mal.
Lectura adicional
- “JavaScript: The Good Parts” de Douglas Crockford
- “You Don’t Know JS: Async & Performance” de Kyle Simpson
- MDN Web Docs: Manejo de errores
Source:
https://dzone.com/articles/advanced-error-handling-in-javascript-custom-error