Pregunta 1Si haces await fetch(url) con una URL cuyo dominio no existe, ¿qué error.name llega a catch?
Manejo de errores y tiempos de espera en fetch — AbortController y reintentos
Errores HTTP que fetch no lanza, errores de red en los que falla el propio fetch, límites de tiempo con AbortController y setTimeout, y un solo reintento.
Si lees response.ok, sabes cuándo el servidor responde que un archivo no existe. Pero si no hay conexión, la línea de await fetch lanza una excepción antes de que llegues a ok. Y si la respuesta tarda, el código se queda esperando sin pasar a la siguiente línea.
En este artículo verás cómo recibir cada tipo de fallo de fetch como una excepción y cómo cancelar una solicitud con AbortController.
Convertir las respuestas fallidas en excepciones — throw new Error
Imagina que tres pantallas distintas cargan la lista de pedidos. Si loadJson devuelve null cuando falla, como en el artículo anterior, cada pantalla tiene que revisar si recibió null, y si una se olvida, la línea que lee el valor más adelante lanza un error.
Con un error HTTP (la solicitud llegó al servidor, pero volvió con un status como 403 o 404 que indica un fallo), fetch no lanza ninguna excepción. Si haces throw new Error(...) cuando ok es false, quien llama puede manejar el fallo en un solo lugar con try / catch.
// Si ok es false, lanza una excepción que incluye el status
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${path}`);
}
return await response.json();
}
try {
const orders = await loadJson("fixtures/es/order.json"); // esta línea lanza la excepción
console.log(`Pedidos: ${orders.length}`); // nunca se ejecuta
} catch (error) {
// 403 en la consola de los ejercicios; 404 u otro código en otros servidores
console.log(error.message); // HTTP 403: fixtures/es/order.json
}
Con throw, aunque una pantalla se olvide del catch, el error sin capturar sigue diciendo HTTP 403. En lugar de escribir en cada pantalla un if que revise si hay null, solo tienes que decidir dónde capturar el fallo.
Distinguir los fallos de red — TypeError e instanceof
Cuando estás fuera y pierdes la señal, la solicitud nunca llega al servidor y no vuelve ninguna respuesta. Este fallo termina en el mismo catch que un error HTTP, así que, tal como está el código, mostrarías el mismo mensaje para «el archivo no existe» y para «no hay conexión».
Con un error de red (un fallo en el que no llega ninguna respuesta, por ejemplo porque se cortó la conexión o el dominio no existe), la Promise que devuelve fetch pasa a rejected con un TypeError. Si revisas instanceof TypeError en catch, puedes distinguirlo de un Error que lanzaste tú.
// loadJson es la misma de la sección anterior (lanza una excepción si ok es false)
const urls = [
"fixtures/es/order.json", // llega al servidor, pero el archivo no existe
"https://shop.invalid/orders.json", // un dominio .invalid nunca se conecta a ningún lado
];
for (const url of urls) {
try {
await loadJson(url);
} catch (error) {
// ¿Es un Error que lanzaste tú o un TypeError que lanzó fetch?
const kind = error instanceof TypeError ? "Sin conexión" : "Falló la respuesta";
console.log(`${kind} / ${error.name}: ${error.message}`);
}
}
// Falló la respuesta / Error: HTTP 403: fixtures/es/order.json
// Sin conexión / TypeError: Failed to fetch
- Cualquiera de los dos fallos lanza una excepción en esta línea y pasa a
catch - En
catch, revisaerror instanceof TypeErrorpara distinguirlos
if (!response.ok)— con…/es/order.jsonllega una respuesta yokesfalsethrow new Error(...)— unErrorque lanzas tú
https://shop.invalid/…— no llega ninguna respuestaTypeError: Failed to fetch— lo lanza fetch
Un TypeError del fetch interno sale de loadJson sin llegar a su if. Como cada pantalla quiere mostrar su propio mensaje, loadJson solo lanza la excepción, y el catch de quien llama, que recibe los dos tipos, revisa de cuál se trata.
El mensaje del error de red cambia según el navegador
El message del TypeError que lanza fetch depende del navegador: Failed to fetch en Chrome y Load failed en Safari. Si lo compruebas comparando el texto del mensaje, en otros navegadores terminarás en la rama equivocada. Revisa el tipo con instanceof TypeError.
Cancelar una solicitud por tiempo — AbortController
Un servidor saturado puede tardar decenas de segundos en responder. fetch no tiene ninguna opción para fijar un tiempo máximo de espera, así que la línea de await fetch no avanza hasta que llega la respuesta, y la pantalla se queda cargando. El código de esta sección usa slowFetch, un sustituto de fetch que tarda 300ms en responder.
Si creas un AbortController (un objeto que permite cancelar un fetch en curso) y lo pasas como fetch(url, { signal: controller.signal }), al llamar a abort() la solicitud falla con un AbortError.
Para cancelar una llamada programada, pasa a clearTimeout el ID del temporizador (un número que identifica la llamada programada) que devuelve setTimeout.
// slowFetch es un sustituto de fetch definido en la consola del Ejercicio 3; tarda 300ms en responder y se usa igual que fetch
const controller = new AbortController();
// Programa abort() para dentro de 100ms y guarda el ID del temporizador
const timerId = setTimeout(() => controller.abort(), 100);
try {
// Igual que con fetch, pasa signal en el segundo argumento
const response = await slowFetch("fixtures/es/orders.json", { signal: controller.signal });
console.log(response.ok); // la respuesta tarda 300ms, así que esto nunca se ejecuta
} catch (error) {
console.log(error.name); // AbortError
} finally {
clearTimeout(timerId); // cancela la llamada programada por si la respuesta llega antes
}
Poner clearTimeout en finally hace que no quede ninguna llamada programada, tanto si la solicitud sale bien como si falla. Si abort() se ejecuta antes de terminar de leer el cuerpo, json() también falla con un AbortError, así que deja return await response.json() dentro del try y pasa a finally solo después de leer el cuerpo.
Volver a intentarlo una vez — reintentos y return await
En un celular dentro de un tren en movimiento, la conexión puede cortarse un momento y la solicitud falla, pero si lo intentas de nuevo enseguida, muchas veces funciona. En lugar de pedirle al usuario que recargue cada vez, el código puede reintentar solo y mantener los datos en pantalla.
Para hacer un reintento (volver a enviar una solicitud fallida a la misma URL), llama otra vez a la función dentro de catch. Sin un límite de intentos, el código seguiría enviando solicitudes mientras durara el corte. Por eso, si el segundo intento también falla, el error pasa directamente a quien llama.
// loadJson es la misma de la primera sección. Reintenta una sola vez, y solo con fallos de red y tiempos agotados
async function loadWithRetry(path) {
try {
return await loadJson(path);
} catch (error) {
const retryable = error instanceof TypeError || error.name === "AbortError";
if (!retryable) {
throw error; // si lo reenvías, vuelve el mismo 403 o 404
}
console.log(`Reintentando tras ${error.name}`); // Reintentando tras TypeError
return await loadJson(path); // un segundo fallo pasa directo a quien llama
}
}
try {
await loadWithRetry("https://shop.invalid/orders.json");
} catch (error) {
console.log(`Falló dos veces: ${error.name}`); // Falló dos veces: TypeError
}
Un error HTTP, como el de fixtures/es/order.json, se vuelve a lanzar en el primer catch, así que pasa a quien llama sin una segunda llamada. La tabla de abajo muestra cómo reconocer cada tipo de fallo en catch y si conviene reintentarlo.
| Fallo | Cómo detectarlo en catch | ¿Reintentar? |
|---|---|---|
| Error de red | error instanceof TypeError | Una vez (la red puede volver) |
| Tiempo agotado (abort()) | error.name === "AbortError" | Una vez (puede bajar la carga) |
| Error HTTP (403, 404) | Cualquier otro | No (misma respuesta) |
return sin await no pasa por catch
Si escribes try { return loadJson(path); } sin await, la función sale del try en cuanto devuelve la Promise. Aunque esa Promise pase a rejected después, catch no se ejecuta y el fallo pasa directo a quien llama. Escribe return await loadJson(path);.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Programas abort() para dentro de 100ms y pasas signal a una solicitud que tarda 300ms en responder. ¿Qué pasa?
Pregunta 3¿Qué fallo vuelve a lanzar el loadWithRetry de este artículo sin reintentarlo?