Peticiones HTTP con fetch en JavaScript: GET, POST y errores

fetch es la forma estándar de hacer peticiones HTTP en JavaScript. Viene de serie en todos los navegadores y en Node.js desde la versión 18, con la misma sintaxis en los dos sitios.

Para que los ejemplos se puedan reproducir sin depender de nadie, las peticiones van contra el pequeño servidor de nuestro ejemplo de servidor web, arrancado con node servidor.mjs en otra terminal. Funcionan igual con cualquier API de internet: solo cambia la URL.

Una petición GET

const res = await fetch("http://localhost:3000/api/productos");
console.log(res.status, res.headers.get("content-type"));

const productos = await res.json();
for (const p of productos) {
  console.log(`${p.id}. ${p.nombre}: $${p.precio}`);
}
salida200 application/json; charset=utf-8
1. Teclado: $49.9
2. Ratón: $19.5

Hay dos await, y cada uno espera una cosa distinta. El primero, el de fetch, termina en cuanto llegan el código de estado y las cabeceras. El cuerpo puede seguir llegando después, y res.json() espera a tenerlo entero y lo convierte en un objeto. Si en vez de JSON quieres el texto tal cual, res.text().

Una llamada a fetch: la petición, la respuesta con el estado y, después, el cuerpoQué espera cada await de fetchtu programael servidor1. la petición: método, URL, cabeceras y cuerpoconst res = await fetch(url)2. llegan el estado y las cabecerasres.ok, res.status, res.headers3. después llega el cuerpoconst datos = await res.json()Red caída o tiempo agotadofetch lanza una excepción:hay que usar try/catchError 404 o 500fetch NO lanza nada:hay que mirar res.ok

Las tres fases de una petición y los dos casos de error, que se tratan de forma distinta. Esquema: decodigo.com.

Enviar datos: POST con JSON

const res = await fetch("http://localhost:3000/api/productos", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ nombre: "Monitor", precio: 189 }),
});

if (!res.ok) {
  throw new Error(`El servidor respondió ${res.status}`);
}
const creado = await res.json();
console.log(res.status, creado);
salida201 { id: 3, nombre: 'Monitor', precio: 189 }

Tres cosas que no se pueden olvidar: el method, la cabecera Content-Type para que el servidor sepa qué le llega, y el cuerpo convertido a texto con JSON.stringify. El 201 significa «creado».

Un 404 no es una excepción

Esta es la sorpresa más común con fetch. Si el servidor responde, aunque sea con un error, la promesa se cumple con normalidad:

const res = await fetch("http://localhost:3000/api/productos/99");
console.log("¿lanzó una excepción? no");
console.log(res.ok, res.status, res.statusText);
console.log(await res.json());
salida¿lanzó una excepción? no
false 404 Not Found
{ error: 'No existe ese producto' }

Para fetch, la petición ha ido bien: preguntó y le contestaron. Que la respuesta sea un 404 o un 500 es asunto tuyo, y se mira con res.ok, que es true solo para los códigos del 200 al 299.

Una función que hace las dos comprobaciones

En un programa real conviene envolver fetch en una función que compruebe res.ok y que no espere para siempre:

async function pedirJSON(url, opciones = {}) {
  const res = await fetch(url, {
    ...opciones,
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) {
    throw new Error(`${res.status} ${res.statusText} en ${url}`);
  }
  return res.json();
}

try {
  const p = await pedirJSON("http://localhost:3000/api/productos/1");
  console.log("bien:", p.nombre);
  await pedirJSON("http://localhost:3000/api/productos/99");
} catch (err) {
  console.log("falló:", err.message);
}
salidabien: Teclado
falló: 404 Not Found en http://localhost:3000/api/productos/99

AbortSignal.timeout(5000) cancela la petición si tarda más de cinco segundos. Sin él, una petición a un servidor colgado puede quedarse esperando minutos.

Los errores que sí lanzan una excepción

Lo que hace fallar a fetch es no poder hablar con el servidor: que no haya red, que el servidor esté apagado o que se acabe el tiempo. Con el servidor apagado:

try {
  await fetch("http://localhost:3999/");
} catch (err) {
  console.log(err.name + ": " + err.message);
  console.log("causa:", err.cause.code);
}
salidaTypeError: fetch failed
causa: ECONNREFUSED

El mensaje, fetch failed, dice poco; la explicación está en err.cause. ECONNREFUSED significa que nadie escucha en ese puerto. Y si el tiempo límite se agota:

con AbortSignal.timeout(1000)TimeoutError: The operation was aborted due to timeout
Lo que pasa¿fetch lanza?Cómo se detecta
El servidor responde 200Nores.ok es true
El servidor responde 404 o 500Nores.ok es false; mira res.status
Servidor apagado o sin redSí: TypeError: fetch failedtry/catch y err.cause.code
Se agota el tiempoSí: TimeoutErrortry/catch y err.name
En el navegador, lo mismo, más una cosa: CORS. Una página solo puede leer respuestas de otro dominio si ese servidor lo permite con la cabecera Access-Control-Allow-Origin. Si no, fetch falla con un TypeError y el detalle aparece solo en la consola. No es un fallo de tu código: es el servidor el que tiene que autorizarlo.

Todo el código de esta página se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; las salidas, incluidos los errores, están copiadas de esa ejecución. Documentación oficial: Uso de Fetch en MDN.