Saltar al contenido
fetch.js · devschool

fetch, promesas y async/await

Lección 33 de 38 · 6 min de lectura · Actualizado el

En esta lección
  1. El problema: las cosas que tardan
  2. Qué es una promesa
  3. async y await
  4. Los dos pasos de fetch
  5. Comprobar el código de estado
  6. Gestionar los errores
  7. Enviar datos: POST, PUT y DELETE
  8. Construir la URL con cuidado
  9. Varias peticiones a la vez
  10. La política del mismo origen y CORS
  11. La forma antigua: .then()

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:

  1. El navegador carga la página y su JavaScript.
  2. El JavaScript hace peticiones HTTP a un servidor (normalmente a una API REST), sin que el usuario haga nada especial.
  3. El servidor responde, casi siempre con JSON.
  4. 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:

  1. Si una función usa await, debe ser async. Si no, verás el error await is only valid in async functions.
  2. Una función async siempre devuelve una promesa. Así que quien la llame también tendrá que usar await (y ser async a 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 con JSON.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).

fetch.js
Consola

Pon a prueba lo que has aprendido

[JavaScript] Ves [object Promise] en tu página en lugar de los datos. ¿Qué falta?

[JavaScript] El servidor responde con un 404. ¿fetch lanza un error?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.