fetch() ahora está integrado en todos los navegadores modernos y Node.js 18+.
Reemplazó a XMLHttpRequest y eliminó la necesidad de Axios en la mayoría de los proyectos.
Pero el uso por defecto que muestra cada tutorial — fetch(url).then(r => r.json())
— omite el manejo de errores, no tiene tiempo de espera y falla en cualquier entorno de producción real.
Esta guía cubre los patrones que realmente resisten.
Lo básico — GET y POST
fetch() devuelve una Promise que se resuelve con un
objeto Response.
Una petición GET es sencilla:
const response = await fetch('https://api.example.com/products');
const products = await response.json();Una petición POST con un cuerpo JSON necesita un poco más de configuración:
const newProduct = {
name: 'Wireless Keyboard',
price: 79.99,
category: 'electronics'
};
const response = await fetch('https://api.example.com/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(newProduct)
});
const created = await response.json();
console.log(`Created product with id: ${created.id}`);Content-Type: application/json
en las peticiones POST. Sin él, muchos frameworks de servidor no parsearán el cuerpo, y obtendrás un
error 400 Bad Request o un cuerpo de petición vacío sin ningún mensaje de error útil.La verificación de errores en dos pasos — response.ok
Este es el patrón más importante para interiorizar. fetch() solo rechaza
su Promise en errores de red (sin conexión, fallo DNS, bloqueo CORS). Una respuesta 404, 401 o 500
todavía resuelve la Promise — con response.ok establecido en
false. Si no verificas esto, pasarás silenciosamente las respuestas de error a
response.json():
// ❌ Broken — 404 and 500 responses appear to succeed
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
return await response.json(); // parses the error body as if it were data
}
// ✅ Correct — check response.ok before parsing
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
// Try to get the error message from the body if it's JSON
const errorBody = await response.json().catch(() => null);
const message = errorBody?.message ?? `HTTP ${response.status}: ${response.statusText}`;
throw new Error(message);
}
return await response.json();
}response.ok es true para los códigos de estado 200–299.
Todo lo demás — redirecciones 301 (si no se siguen automáticamente), errores 400, errores 500 — lo establece
en false. Siempre verifícalo antes de parsear.
AbortController — Tiempos de espera y cancelación
fetch() no tiene tiempo de espera integrado. Una petición puede bloquearse indefinidamente
si el servidor deja de responder a mitad de la transferencia. La solución es
AbortController:
async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/products', {}, 5000);
} catch (err) {
console.error(err.message); // "Request timed out after 5000ms"
}AbortController también es útil para cancelar peticiones en vuelo cuando un usuario navega a otra página o realiza una nueva búsqueda antes de que la anterior se complete:
let searchController = null;
async function searchProducts(query) {
// Cancel any previous search request
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(
`/api/products/search?q=${encodeURIComponent(query)}`,
{ signal: searchController.signal }
);
return await response.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
return null; // request was cancelled — that's OK
}
}Reintento con retroceso exponencial
Las peticiones de red fallan de forma transitoria — un 503 en un reintento a menudo tiene éxito en el siguiente. El retroceso exponencial es la estrategia estándar: esperar progresivamente más tiempo entre reintentos para evitar sobrecargar un servidor ya sobrecargado:
async function fetchWithRetry(url, options = {}, retries = 3, baseDelayMs = 500) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx) — they won't fix themselves
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error ${response.status} — not retrying`);
}
if (!response.ok) {
throw new Error(`Server error ${response.status}`);
}
return await response.json();
} catch (err) {
const isLastAttempt = attempt === retries;
if (isLastAttempt || err.message.includes('Client error')) {
throw err;
}
// Exponential backoff with jitter: 500ms, 1000ms, 2000ms + random
const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const products = await fetchWithRetry('/api/products', {}, 3, 500);Math.random() * 100 al
retraso previene el "thundering herd" — donde miles de clientes reintentan exactamente al
mismo momento después de un problema en el servidor. Un pequeño desplazamiento aleatorio, un gran beneficio en fiabilidad.Un patrón interceptor — Envolviendo fetch()
Axios popularizó el concepto de interceptor: un hook que se ejecuta antes de cada petición
y después de cada respuesta. Puedes construir lo mismo como un wrapper ligero alrededor de
fetch():
// api.js — your project's fetch wrapper
const API_BASE = 'https://api.example.com';
function getAuthToken() {
return localStorage.getItem('authToken');
}
async function apiFetch(path, options = {}) {
const url = `${API_BASE}${path}`;
// Request interceptor — add auth header to every request
const headers = {
'Content-Type': 'application/json',
...options.headers
};
const token = getAuthToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
const response = await fetch(url, { ...options, headers });
// Response interceptor — handle auth expiry globally
if (response.status === 401) {
logout(); // token expired — redirect to login
throw new Error('Session expired');
}
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.message ?? `API error ${response.status}`);
}
// Return null for 204 No Content
if (response.status === 204) return null;
return response.json();
}
// Usage — clean, no repeated boilerplate
const products = await apiFetch('/products');
const created = await apiFetch('/products', {
method: 'POST',
body: JSON.stringify({ name: 'New Product', price: 29.99 })
});Este patrón centraliza la autenticación, el manejo de errores y la configuración de la URL base. Cada llamada API en tu base de código obtiene el mismo comportamiento de forma gratuita. Cuando los requisitos cambian — como agregar un nuevo encabezado o cambiar de JWT a cookies de sesión — actualizas un solo lugar.
Manejo de diferentes tipos de respuesta
No todas las API devuelven JSON. fetch() tiene métodos para todos los tipos de respuesta comunes:
// JSON (most common)
const data = await response.json();
// Plain text (CSV, HTML, logs)
const csvText = await response.text();
// Binary data — download a file
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href = blobUrl;
downloadLink.download = 'export.csv';
downloadLink.click();
URL.revokeObjectURL(blobUrl); // clean up
// ArrayBuffer — for WebAssembly or typed arrays
const buffer = await response.arrayBuffer();
const byteArray = new Uint8Array(buffer);
// Form data (multipart responses)
const formData = await response.formData();CORS — Lo que los desarrolladores necesitan saber
CORS (Cross-Origin Resource Sharing) es aplicado por el navegador, no por el servidor.
Cuando tu frontend en app.example.com hace un fetch desde api.example.com,
el navegador envía un encabezado Origin y verifica la respuesta para obtener permiso.
Esto es lo que controlas desde el lado del cliente:
// credentials: 'include' — send cookies/session tokens cross-origin
// The server must also respond with Access-Control-Allow-Credentials: true
const response = await fetch('https://api.example.com/account', {
credentials: 'include'
});
// credentials: 'same-origin' — default, only sends credentials to same origin
// credentials: 'omit' — never send credentials (useful for public CDN requests)
// mode: 'no-cors' — fire-and-forget for cross-origin requests
// You get a "opaque" response — no status, no body, no error
await fetch('https://analytics.example.com/event', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify({ event: 'page_view' })
});
// Use for analytics pings where you don't need the responseSi obtienes errores CORS, la solución casi siempre está en el servidor: el servidor
necesita agregar Access-Control-Allow-Origin a sus encabezados de respuesta.
La guía CORS de MDN
es la referencia más completa sobre cómo configurarlo.
Herramientas útiles
Cuando depuras respuestas fetch, JSON Formatter hace legibles los payloads de API y JSON Validator detecta respuestas malformadas. Para codificar parámetros de consulta de forma segura, JSON URL Encode te tiene cubierto. La guía MDN Using Fetch es la mejor referencia única para todas las opciones de fetch y métodos de respuesta.
Conclusión
fetch() es una base sólida para las peticiones HTTP en JavaScript, pero
los valores por defecto omiten todo lo que necesitas en producción. Siempre verifica response.ok
antes de parsear. Agrega tiempos de espera con AbortController. Construye una función wrapper ligera
para manejar encabezados de autenticación, normalización de errores y redirecciones 401 en un solo lugar. Agrega lógica de reintento
con retroceso exponencial para peticiones que pueden fallar de forma transitoria. Esos cuatro hábitos son la
diferencia entre código de demostración y código de producción.