fetch, promesas y async/await
En esta lección
Casi todas las aplicaciones web modernas piden datos a un servidor sin recargar la página: un chat que recibe mensajes, un buscador que sugiere resultados mientras escribes, un mapa que carga la zona a la que te mueves. Esta técnica se popularizó en 2005 con Gmail y Google Maps bajo el nombre de AJAX, y hoy se hace con la función fetch.
El funcionamiento es siempre el mismo:
- El navegador carga la página y su JavaScript.
- El JavaScript hace peticiones HTTP a un servidor (normalmente a una API REST), sin que el usuario haga nada especial.
- El servidor responde, casi siempre con JSON.
- El JavaScript actualiza la página con los datos recibidos.
El problema: las cosas que tardan
Pedir datos a un servidor puede tardar milisegundos o varios segundos. JavaScript no puede quedarse esperando: la página se congelaría. Por eso fetch no devuelve los datos, sino una promesa.
Qué es una promesa
Una promesa es un objeto que representa un valor que todavía no existe, pero existirá. Es como el ticket que te dan en una cafetería: aún no tienes el café, pero tienes la promesa de que llegará. Una promesa puede:
- Cumplirse (fulfilled): el valor llega.
- Rechazarse (rejected): algo ha fallado y, en lugar del valor, llega un error.
async y await
Para esperar el valor de una promesa se usa await:
const respuesta = await fetch("/api/productos");
await pausa esa función hasta que la promesa se cumpla, y te da el valor. El resto de la página sigue funcionando mientras tanto.
await solo se puede usar dentro de funciones marcadas con async:
async function cargarProductos() {
const respuesta = await fetch("/api/productos");
const productos = await respuesta.json();
return productos;
}
Dos reglas que conviene entender bien:
- Si una función usa
await, debe serasync. Si no, verás el errorawait is only valid in async functions. - Una función
asyncsiempre devuelve una promesa. Así que quien la llame también tendrá que usarawait(y serasynca su vez), y así hacia arriba.
async function mostrarProductos() {
const productos = await cargarProductos(); // ← hace falta await
pintar(productos);
}
Si olvidas el await, no obtendrás los datos sino la propia promesa. Si ves Promise { <pending> } o [object Promise] en tu página, te falta un await.
La cadena termina en un manejador de eventos o en el nivel superior de un módulo:
boton.addEventListener("click", async () => {
await mostrarProductos();
});
Los dos pasos de fetch
const respuesta = await fetch(url); // 1. la respuesta HTTP "en bruto"
const datos = await respuesta.json(); // 2. el contenido, convertido de JSON
fetch devuelve un objeto Response con la respuesta completa (cabeceras, código de estado…). Para obtener el contenido hay que leerlo con otro await: .json() si es JSON, .text() si es texto.
Comprobar el código de estado
Cada respuesta HTTP trae un código de estado: 200 si todo ha ido bien, 404 si no existe lo que pides, 500 si el servidor ha fallado…
Cuidado: fetch solo lanza un error si no hay conexión. Si el servidor responde con un 404 o un 500, para fetch eso es una respuesta válida. Tienes que comprobarlo tú:
async function cargarJson(url) {
const respuesta = await fetch(url);
if (!respuesta.ok) { // ok es true si el status es 200-299
throw new Error(`Error ${respuesta.status} al cargar ${url}`);
}
return respuesta.json();
}
Si el código no es correcto, no intentes leer el JSON: probablemente no lo haya.
Gestionar los errores
Las peticiones fallan: el móvil pierde la cobertura, el servidor está caído, la URL ha cambiado. Envuelve las llamadas en try...catch y muestra algo útil al usuario:
async function mostrarProductos() {
estado.textContent = "Cargando...";
try {
const productos = await cargarJson("/api/productos");
pintar(productos);
estado.textContent = "";
} catch (error) {
estado.textContent = "No se han podido cargar los productos. Inténtalo de nuevo.";
console.error(error);
}
}
Enviar datos: POST, PUT y DELETE
Por defecto, fetch hace una petición GET (pedir datos). Para enviar datos, se indica el método y el cuerpo:
const nuevaTarea = { titulo: "Estudiar fetch", hecha: false };
const respuesta = await fetch("/api/tareas", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(nuevaTarea),
});
const tareaCreada = await respuesta.json();
method:"POST"para crear,"PUT"o"PATCH"para modificar,"DELETE"para borrar.headers: le dice al servidor que le envías JSON.body: los datos, convertidos a texto conJSON.stringify.
Construir la URL con cuidado
Al unir fragmentos de una URL es muy fácil acabar con dos barras seguidas o sin ninguna (/api//dobla/3 o http://localhost:3000api/...). Y los valores que escribe el usuario pueden contener espacios o caracteres especiales. Usa URL y URLSearchParams:
const url = new URL("/api/buscar", "http://localhost:3000");
url.searchParams.set("q", "tortilla de patatas");
url.searchParams.set("pagina", 2);
url.toString(); // "http://localhost:3000/api/buscar?q=tortilla+de+patatas&pagina=2"
Varias peticiones a la vez
Si necesitas varias cosas que no dependen unas de otras, pídelas en paralelo con Promise.all:
const [usuario, pedidos] = await Promise.all([
cargarJson("/api/usuario"),
cargarJson("/api/pedidos"),
]);
Tarda lo que la más lenta, no la suma de las dos.
La política del mismo origen y CORS
Por seguridad, el navegador aplica la política del mismo origen (same-origin policy): el JavaScript de una página solo puede leer respuestas de su mismo origen (mismo protocolo, dominio y puerto).
¿Por qué? Imagina que tienes abierta la web de tu banco en una pestaña. Si cualquier otra web pudiera hacer peticiones al banco desde tu navegador y leer las respuestas, podría ver tus movimientos. La política lo impide.
Para permitir peticiones entre orígenes distintos existe CORS (Cross-Origin Resource Sharing): el servidor añade una cabecera (Access-Control-Allow-Origin) indicando qué otros orígenes pueden leer sus respuestas. Muchas APIs públicas la incluyen, y por eso puedes llamarlas desde tu página. Si una API no la incluye, verás en la consola un error de CORS, y no se soluciona desde el navegador: lo tiene que configurar el servidor (o tienes que pedir los datos desde tu propio servidor).
La solución más sencilla y segura para tus proyectos: que el mismo servidor sirva tu página y tu API. Así todo es del mismo origen. Lo verás en la lección de API REST.
(En tutoriales antiguos verás JSONP, un truco de 2005 para saltarse esta política cargando los datos como si fueran un script. Está obsoleto y tiene problemas de seguridad: hoy se usa CORS.)
La forma antigua: .then()
Antes de async/await (2017) las promesas se usaban encadenando .then() y .catch(). Lo verás en mucho código:
fetch("/api/productos")
.then((respuesta) => respuesta.json())
.then((productos) => pintar(productos))
.catch((error) => console.error(error));
Hace lo mismo. async/await se lee de arriba abajo, como el resto del código, y suele ser más claro. Y aún más antiguo, con jQuery, verás $.ajax(...) o el objeto XMLHttpRequest: hoy fetch los sustituye a ambos.
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.