Manejo y propagación de errores en JavaScript | Nicolás Garzón
Un error debería manejarse en la capa que puede tomar una decisión útil.
Texto
Copiar origen del fallo
↓
propagación
↓
frontera que entiende el contexto
↓
recuperar, traducir, reportar o finalizarJavaScript
Copiar function loadProducts ( ) {
return repository. findAll ( ) ;
} Si findAll lanza y loadProducts no captura, el error continúa hacia su consumidor.
JavaScript
Copiar try {
return repository. findAll ( ) ;
} catch ( error) {
throw error;
} La versión sin try comunica lo mismo y conserva el stack sin ruido adicional.
Una capa debería capturar cuando puede:
Recuperarse.
Traducir el error a su abstracción.
Añadir contexto útil.
Limpiar recursos.
Registrar o reportar en el límite definido.
Convertir el fallo en una respuesta pública.
Si no puede hacer ninguna, normalmente debe dejarlo propagarse.
JavaScript
Copiar try {
return readFromCache ( key) ;
} catch ( error) {
if ( error instanceof CacheUnavailableError ) {
return readFromDatabase ( key) ;
}
throw error;
} La capa conoce una alternativa válida.
Un fallback debe conservar el significado. Retornar [] ante un fallo de autorización no significa “no hay datos”.
JavaScript
Copiar try {
return repository. findById ( productId) ;
} catch ( cause) {
throw new ProductQueryError (
"Product lookup failed" ,
{
productId,
cause,
} ,
) ;
} El servicio no obliga a sus consumidores a conocer detalles del driver de base de datos.
La traducción debe conservar la causa y aportar una abstracción mejor, no cambiar un error específico por otro más vago.
JavaScript
Copiar try {
return parseOrder ( payload) ;
} catch ( cause) {
throw new InvalidOrderPayloadError (
"Incoming order payload is invalid" ,
{
requestId,
cause,
} ,
) ;
} El contexto útil responde preguntas como:
¿Qué operación falló?
¿Qué entidad estaba involucrada?
¿Qué request o job lo ejecutó?
¿Qué etapa se alcanzó?
No agregues datos sensibles.
Texto
Copiar repository log
service log
controller log
middleware log
monitoring logEl mismo error genera cinco eventos y parece que ocurrieron cinco fallos.
Capas internas añaden contexto mediante cause o propiedades.
La frontera principal registra una vez.
Métricas específicas pueden contar categorías sin duplicar la traza completa.
Middleware HTTP.
Handler de una tarea.
Event handler superior.
Comando CLI.
Error boundary de UI.
Inicialización de la aplicación.
JavaScript
Copiar {
code : "REPOSITORY_UNAVAILABLE" ,
message : "Connection timed out after 3000 ms" ,
cause : error,
} JavaScript
Copiar {
code : "PRODUCTS_UNAVAILABLE" ,
message : "No pudimos cargar los productos." ,
} No devuelvas automáticamente stack traces, consultas, rutas internas o credenciales.
Para una operación donde el fallo forma parte del flujo cotidiano:
JavaScript
Copiar function authenticate ( credentials ) {
if ( ! matches ( credentials) ) {
return {
success : false ,
reason : "INVALID_CREDENTIALS" ,
} ;
}
return {
success : true ,
user,
} ;
} El consumidor debe tratar ambos resultados.
Para un fallo inesperado de infraestructura:
JavaScript
Copiar throw new AuthenticationServiceError (
"Authentication service is unavailable" ,
{ cause } ,
) ; No conviertas todos los resultados esperados en excepciones ni todos los fallos inesperados en false.
JavaScript
Copiar try {
await saveOrder ( order) ;
} catch ( error) {
console. log ( error) ;
}
return { success : true } ; La función afirma éxito aunque guardar falló.
Relanzar.
Retornar un resultado de fallo.
Compensar la operación.
Marcar estado parcial.
Recuperarse con una alternativa real.
Un reintento puede ayudar con fallos transitorios:
Timeout.
Servicio temporalmente ocupado.
Conexión interrumpida.
Datos inválidos.
Permisos insuficientes.
Recurso inexistente.
Bug de programación.
Antes de reintentar pregunta:
¿La operación es idempotente?
¿Podría duplicar un pago o pedido?
¿Cuántos intentos están permitidos?
¿Existe espera entre intentos?
¿Qué errores son transitorios?
¿La cancelación sigue siendo posible?
JavaScript
Copiar async function retry ( operation, {
attempts = 3 ,
shouldRetry,
} ) {
let lastError;
for ( let attempt = 1 ; attempt <= attempts; attempt++ ) {
try {
return await operation ( ) ;
} catch ( error) {
lastError = error;
if (
attempt === attempts ||
! shouldRetry ( error)
) {
throw error;
}
}
}
throw lastError;
} La estrategia temporal se completará en asincronía.
JavaScript
Copiar await saveOrder ( order) ;
await sendConfirmation ( order) ; Si la segunda falla, la orden puede estar guardada sin correo.
Eso no es necesariamente un fracaso total. El diseño puede:
Guardar el estado “notification_pending”.
Reintentar la notificación mediante una tarea.
Retornar éxito de orden con una advertencia interna.
Ejecutar una compensación si el dominio lo requiere.
Las excepciones no definen por sí solas atomicidad.
JavaScript
Copiar const resource = openResource ( ) ;
try {
return useResource ( resource) ;
} finally {
resource. close ( ) ;
} La capa puede limpiar sin capturar ni modificar el error.
Navegadores y runtimes suelen permitir observar errores no capturados y rechazos no manejados.
Esos handlers sirven como última red para:
Telemetría.
Diagnóstico.
Finalización controlada del proceso cuando corresponda.
No sustituyen manejo local. En ese punto quizá ya no exista una forma segura de continuar la operación.
JavaScript
Copiar try {
const product = await loadProduct ( ) ;
} catch ( error) {
handle ( error) ;
} Sin await, el try no espera el resultado:
JavaScript
Copiar try {
loadProduct ( ) ;
} catch {
} La promesa debe retornarse, esperarse o recibir un .catch.
JavaScript
Copiar async function findProduct ( id ) {
try {
return await database. products. find ( id) ;
} catch ( cause) {
throw new RepositoryError (
"Product query failed" ,
{ cause, entityId : id } ,
) ;
}
} JavaScript
Copiar async function requireProduct ( id ) {
const product = await findProduct ( id) ;
if ( ! product) {
throw new ProductNotFoundError ( id) ;
}
return product;
} JavaScript
Copiar async function handleRequest ( request ) {
try {
const product = await requireProduct (
request. params. id,
) ;
return json ( product, 200 ) ;
} catch ( error) {
reportError ( error, {
requestId : request. id,
} ) ;
return toHttpResponse ( error) ;
}
} Cada capa aporta algo distinto y la traza completa se registra una vez.
Capturar en todas las funciones.
Registrar el mismo error en cada capa.
Retornar un fallback que cambia el significado.
Traducir a un error más genérico y perder información.
Exponer detalles internos al usuario.
Reintentar errores permanentes.
Reintentar operaciones no idempotentes sin protección.
Ignorar estados parciales.
Confiar en un handler global como estrategia principal.
Iniciar una promesa dentro de try sin esperarla ni retornarla.
Maneja un error donde exista una decisión útil.
La propagación automática evita try/catch redundantes.
Recuperar, traducir, contextualizar, limpiar y reportar son responsabilidades diferentes.
Registra el fallo completo una vez en una frontera clara.
Separa diagnóstico técnico de mensajes públicos.
Utiliza resultados para fallos esperados y errores para contratos incumplidos o fallos inesperados.
Los reintentos necesitan clasificación, límites e idempotencia.
Una excepción no deshace efectos anteriores.
Los handlers globales son una última red, no el diseño principal.
¿Qué problema hay en este fallback?
JavaScript
Copiar try {
return await loadPrivateOrders ( ) ;
} catch {
return [ ] ;
} Respuesta Convierte cualquier fallo —incluido un problema de permisos, conexión o programación— en “no existen órdenes”. El fallback cambia el significado de la información y oculta la causa. Solo sería válido para una categoría concreta que realmente permita esa alternativa.
Debugging explica cómo observar la ejecución y localizar la causa real de un comportamiento incorrecto.