CAP 06 · LEC 06·Asincronía

Errores asíncronos: captura lo que no puedes ver

Los errores asíncronos son más difíciles que los síncronos: el stack trace desaparece, los try/catch normales no los capturan, y una Promise rechazada sin manejar puede silenciar bugs graves.

● INTERMEDIO8 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

Por qué los errores async son difíciles

Un try/catch síncrono no puede capturar errores que ocurren en el futuro:

// ❌ Este try/catch NO captura el error async try { setTimeout(() => { throw new Error("Error dentro del timeout"); // No capturado }, 100); } catch (err) { console.log("Nunca llega aquí"); } // El error aparece como "Uncaught Error" en la consola del browser // ❌ Tampoco captura una Promise rechazada try { Promise.reject(new Error("Promise rechazada")); // No capturado } catch (err) { console.log("Tampoco llega aquí"); } // ✅ Necesitas capturar donde el error puede ocurrir setTimeout(() => { try { throw new Error("Error dentro del timeout"); } catch (err) { console.log("Capturado:", err.message); // Capturado: Error dentro del timeout } }, 100);
// ❌ Este try/catch NO captura el error async try { setTimeout((): void => { throw new Error("Error dentro del timeout"); }, 100); } catch (err) { console.log("Nunca llega aquí"); } // ❌ Tampoco captura una Promise rechazada try { Promise.reject(new Error("Promise rechazada")); } catch (err) { console.log("Tampoco llega aquí"); } // ✅ Necesitas capturar donde el error puede ocurrir setTimeout((): void => { try { throw new Error("Error dentro del timeout"); } catch (err) { if (err instanceof Error) { console.log("Capturado:", err.message); } } }, 100);
SalidaCapturado: Error dentro del timeout

Errores en Promises: .catch() vs segundo argumento

Hay dos formas de capturar rechazos en .then(), pero no son equivalentes:

function operacionRiesgosa(debeFallar) { return new Promise((resolve, reject) => { setTimeout(() => { if (debeFallar) { reject(new Error("Operación fallida")); } else { resolve("datos"); } }, 50); }); } // ❌ Segundo argumento de .then() — NO captura errores del primer argumento operacionRiesgosa(false).then( datos => { throw new Error("Error al procesar datos"); // No capturado return datos.toUpperCase(); }, err => { console.log("Solo captura rechazos de la Promise original:", err.message); } ); // ✅ .catch() captura TODO lo que falle en la cadena operacionRiesgosa(false) .then(datos => { throw new Error("Error al procesar datos"); }) .catch(err => { console.log("Capturado por .catch():", err.message); // Capturado por .catch(): Error al procesar datos }); // ✅ .catch() para rechazo original operacionRiesgosa(true) .then(datos => datos.toUpperCase()) .catch(err => { console.log("Rechazo original:", err.message); // Rechazo original: Operación fallida });
function operacionRiesgosa(debeFallar: boolean): Promise<string> { return new Promise((resolve, reject) => { setTimeout(() => { if (debeFallar) { reject(new Error("Operación fallida")); } else { resolve("datos"); } }, 50); }); } // ✅ .catch() captura TODO lo que falle en la cadena operacionRiesgosa(false) .then((datos: string) => { throw new Error("Error al procesar datos"); }) .catch((err: Error) => { console.log("Capturado por .catch():", err.message); // Capturado por .catch(): Error al procesar datos }); // ✅ .catch() para rechazo original operacionRiesgosa(true) .then((datos: string) => datos.toUpperCase()) .catch((err: Error) => { console.log("Rechazo original:", err.message); // Rechazo original: Operación fallida });
SalidaCapturado por .catch(): Error al procesar datos Rechazo original: Operación fallida
Siempre prefiere .catch() al final

El segundo argumento de .then(onFulfilled, onRejected) solo captura rechazos de la Promise anterior — no errores lanzados en onFulfilled. .catch() captura ambos. Úsalo siempre.

Errores en async/await con try/catch

Con async/await, los errores de Promises rechazadas se comportan como errores síncronos dentro del bloque try:

async function cargarDatos(url) { const respuesta = await fetch(url); if (!respuesta.ok) { throw new Error(`HTTP ${respuesta.status}: ${respuesta.statusText}`); } return respuesta.json(); } // ✅ Manejo centralizado con try/catch async function procesarPagina() { try { const datos = await cargarDatos("https://api.ejemplo.com/datos"); const procesados = datos.items.map(item => item.nombre); console.log("Items:", procesados); } catch (err) { // Captura tanto errores de red como errores de datos console.error("Fallo al procesar:", err.message); } } // ✅ Patrón: función wrapper que convierte Promise en [error, dato] async function intentar(promesa) { try { const dato = await promesa; return [null, dato]; } catch (err) { return [err, null]; } } async function usarIntento() { const [err, datos] = await intentar(cargarDatos("https://api.ejemplo.com/datos")); if (err) { console.log("Error capturado sin try/catch:", err.message); return; } console.log("Datos recibidos:", datos); }
async function cargarDatos<T>(url: string): Promise<T> { const respuesta = await fetch(url); if (!respuesta.ok) { throw new Error(`HTTP ${respuesta.status}: ${respuesta.statusText}`); } return respuesta.json() as Promise<T>; } // ✅ Patrón: función wrapper que convierte Promise en [error, dato] async function intentar<T>( promesa: Promise<T> ): Promise<[Error, null] | [null, T]> { try { const dato = await promesa; return [null, dato]; } catch (err) { return [err instanceof Error ? err : new Error(String(err)), null]; } } type DatosApi = { items: { nombre: string }[] }; async function usarIntento(): Promise<void> { const [err, datos] = await intentar( cargarDatos<DatosApi>("https://api.ejemplo.com/datos") ); if (err) { console.log("Error capturado sin try/catch:", err.message); return; } console.log("Items:", datos.items.map(i => i.nombre)); }

Errores no capturados: unhandledRejection

Una Promise rechazada sin .catch() ni try/catch produce un unhandledRejection. El comportamiento varía por entorno:

// En Node.js — escuchar rechazos no capturados process.on("unhandledRejection", (razon, promesa) => { console.error("Promise rechazada sin capturar:", razon); // En producción: registrar en sistema de monitoreo // No es recomendable como estrategia principal de manejo de errores }); // En el browser — equivalente window.addEventListener("unhandledrejection", (evento) => { console.error("Rechazo no capturado:", evento.reason); evento.preventDefault(); // Evita que aparezca en la consola }); // ❌ Esta Promise rechazada activa unhandledRejection Promise.reject(new Error("Sin capturar")); // ✅ Siempre captura tus Promises Promise.reject(new Error("Con captura")) .catch(err => console.error("Capturado:", err.message)); // ✅ En Node.js moderno, un unhandledRejection termina el proceso // — es una señal de que el código tiene un bug
// En Node.js — escuchar rechazos no capturados process.on("unhandledRejection", (razon: unknown, promesa: Promise<unknown>) => { const mensaje = razon instanceof Error ? razon.message : String(razon); console.error("Promise rechazada sin capturar:", mensaje); }); // En el browser window.addEventListener("unhandledrejection", (evento: PromiseRejectionEvent) => { const mensaje = evento.reason instanceof Error ? evento.reason.message : String(evento.reason); console.error("Rechazo no capturado:", mensaje); evento.preventDefault(); }); // ❌ Sin captura — activa unhandledRejection Promise.reject(new Error("Sin capturar")); // ✅ Siempre captura tus Promises Promise.reject(new Error("Con captura")) .catch((err: Error) => console.error("Capturado:", err.message));
Node.js 15+ termina el proceso

Desde Node.js 15, un unhandledRejection termina el proceso con código de salida 1. Tratar el evento como "safety net" es una mala práctica — corrígelo en la fuente.

Practica