¿Qué es AJAX?
AJAX significa Asynchronous JavaScript and XML, un término acuñado en 2005 cuando Gmail y Google Maps demostraron por primera vez que una página podía obtener nuevos datos sin necesidad de recargarse por completo. La parte de XML es básicamente historia —las APIs modernas devuelven JSON— pero la idea central sigue siendo la misma: comunicarse con un servidor en segundo plano y actualizar la página en el lugar.
La herramienta original era XMLHttpRequest. Funciona, pero su API es incómoda y se basa en callbacks. El reemplazo moderno es la Fetch API, una interfaz basada en promesas disponible en navegadores, Node.js, Deno y runtimes de edge.
Tu primera solicitud fetch
fetch recibe una URL y devuelve una promesa de un Response.
// basic.js
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);
Eso es todo el flujo ideal. El detalle es que fetch se resuelve incluso para códigos de estado de error, por lo que un 404 no lanza una excepción. Tienes que inspeccionar la respuesta tú mismo.
El objeto Response
El objeto Response te indica qué fue lo que el servidor envió de vuelta.
// response.js
const response = await fetch("/api/users");
response.ok; // true for status 200–299
response.status; // 200, 404, 500, ...
response.statusText;
response.headers.get("content-type");
const data = await response.json(); // parse JSON
const text = await response.text(); // raw text
const blob = await response.blob(); // binary data
const form = await response.formData();
El cuerpo (body) solo puede leerse una vez. Si necesitas tanto el texto plano como el JSON parseado, llama a response.clone() antes de leer, o parsea el texto tú mismo. Ten en cuenta también que response.json() falla si el cuerpo no es un JSON válido; otra razón más para envolver las llamadas en try/catch.
Cómo manejar los errores correctamente
Existen dos tipos de fallos y se comportan de manera diferente.
- Errores de red — sin conexión, fallo de DNS, CORS bloqueado.
fetchse rechaza (rejects), por lo quecatchse encarga de manejarlos. - Errores de HTTP — 404, 401, 500.
fetchse resuelve (resolves); debes verificarresponse.okoresponse.status.
// errors.js
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Could not load user:", error);
throw error;
}
}
Tratar ambos casos de forma explícita es lo que diferencia un código frágil de un código resiliente. La guía de Manejo de Errores cubre la estrategia general.
Envío de datos con POST, PUT y DELETE
Pasa un objeto de opciones para cambiar el método, añadir headers y adjuntar un cuerpo.
// create.js
async function createUser(user) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(user),
});
if (!response.ok) throw new Error("Could not create user");
return response.json();
}
await createUser({ name: "Ada", role: "engineer" });
Usa PUT para reemplazar un recurso, PATCH para actualizar una parte del mismo y DELETE para eliminarlo. Para la subida de archivos, construye un objeto FormData y pásalo como el cuerpo; el navegador configurará automáticamente el content type multipart correcto por ti.
Cabeceras y autenticación
Las cabeceras transportan metadatos sobre la solicitud. Puedes definirlas por solicitud, y muchas API esperan un token de autorización.
// auth.js
const response = await fetch("/api/me", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
Nunca escribas secretos directamente en el código del lado del cliente; cualquier cosa que se envíe al navegador es pública. Los tokens deben provenir de un flujo de inicio de sesión y almacenarse con cuidado.
Cancelar solicitudes
Una solicitud que ya no es relevante —porque el usuario escribió otro carácter, navegó a otra página o el componente se desmontó— debe ser cancelada. AbortController hace exactamente eso.
// abort.js
const controller = new AbortController();
fetch("/api/search?q=javascript", { signal: controller.signal })
.then((res) => res.json())
.then(console.log)
.catch((error) => {
if (error.name === "AbortError") return;
console.error(error);
});
// Cancel when it is no longer needed
controller.abort();
Debido a que fetch no tiene una opción de timeout, AbortController es también la forma estándar de implementar uno.
// timeout.js
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch("/api/slow", { signal: controller.signal });
return await res.json();
} finally {
clearTimeout(timer);
}
CORS y credenciales
Los navegadores aplican la política de mismo origen (same-origin policy): JavaScript no puede leer respuestas de un origen diferente a menos que el servidor lo permita. Ese permiso es CORS, y se configura enteramente en el servidor a través de encabezados de respuesta como Access-Control-Allow-Origin. Si ves un error de CORS, la solución debe aplicarse en la configuración del servidor, no en tu llamada a fetch.
Por defecto, fetch no envía cookies a URLs de orígenes cruzados. Para incluirlas, establece credentials: "include" y asegúrate de que el servidor permita solicitudes con credenciales. Esta es una fuente común de confusión del tipo “funciona en Postman pero no en el navegador”.
Patrones prácticos
Estados de carga, éxito y error. Refleja siempre el ciclo de vida de la solicitud en la UI.
// state.js
async function loadPosts() {
showSpinner();
try {
const res = await fetch("/api/posts");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
renderPosts(await res.json());
} catch (error) {
showError("Could not load posts. Try again.");
} finally {
hideSpinner();
}
}
Búsqueda con debounce. Cancela la solicitud anterior cada vez que comience una nueva, para evitar que respuestas lentas sobrescriban resultados recientes. Combina AbortController con un pequeño retraso de debounce.
Solicitudes paralelas. Usa Promise.all cuando necesites varios endpoints a la vez, y Promise.allSettled cuando un resultado parcial siga siendo útil.
Mejores prácticas
- Verifica siempre
response.okantes de analizar el cuerpo (body). - Establece un encabezado
Acceptexplícito y unContent-Typeal enviar un cuerpo. - Envuelve las solicitudes asíncronas en
try/catchy muestra un mensaje útil. - Cancela las solicitudes obsoletas con
AbortController. - Mantén las llamadas a la API en un módulo pequeño en lugar de dispersar las URLs por los componentes.
- Nunca pongas secretos en el código del lado del cliente.
- Muestra estados de carga y de error para que la UI nunca parezca rota.
Errores comunes
- Asumir que
fetchlanza una excepción en un 404 o 500. - Olvidar
JSON.stringifyen el cuerpo de una solicitud. - Omitir el encabezado
Content-Typey recibir un error de parseo del servidor. - Leer el cuerpo de una respuesta dos veces.
- Ignorar CORS hasta que el navegador bloquea la solicitud en producción.
- Dejar solicitudes sin cancelar y permitir que datos obsoletos ganen una carrera (race condition).
Próximos pasos
Fetch es el puente entre tu front end y el mundo. Combínalo con el DOM para renderizar resultados, async/await para secuenciar el trabajo y el manejo de errores para gestionar los fallos de forma controlada. A partir de ahí, profundiza en tu comprensión de los métodos, códigos de estado y headers, ya que estos definen cada solicitud que envíes.