Fetch API y async/await

Fetch permite hacer peticiones HTTP desde el navegador sin recargar la página. Con async/await el código asincrónico se lee casi igual que el sincrónico.

¿Qué es una petición HTTP?

Cuando el navegador carga una página, hace una petición HTTP al servidor. Con Fetch podés hacer esas mismas peticiones desde JavaScript — para traer datos, enviar formularios, o comunicarte con una API — sin que la página se recargue.

// GET básico — traer datos de una URL
fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then(response => response.json())
    .then(data => {
        console.log(data.title);
    })
    .catch(error => {
        console.error("Error:", error);
    });
¿Qué es una API?

Una API (Application Programming Interface) es un servicio que devuelve datos en formato JSON. En estos ejemplos usamos JSONPlaceholder — una API pública de prueba con posts, usuarios, comentarios y más.

async / await — la forma moderna

En vez de encadenar .then(), podés usar async/await para escribir código asincrónico de forma lineal y más legible. Una función async siempre devuelve una Promise.

// Con .then() — encadenado
fetch(url)
    .then(res => res.json())
    .then(data => mostrar(data))
    .catch(err => console.error(err));

// Con async/await — equivalente, más legible
async function cargarDatos() {
    try {
        const res  = await fetch(url);
        const data = await res.json();
        mostrar(data);
    } catch (error) {
        console.error("Error:", error);
    }
}

cargarDatos();
await solo dentro de async

await solo puede usarse dentro de una función marcada como async. Si estás en el nivel superior del módulo (top-level), podés usar await directamente en navegadores modernos.

Probá el Ejemplo 1: GET básico con fetch

Verificar errores HTTP

fetch() solo lanza un error de red si la conexión falla. Un status 404 o 500 no dispara el catch — hay que verificarlo manualmente con response.ok.

async function cargarUsuario(id) {
    try {
        const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

        // response.ok es true para status 200-299
        if (!res.ok) {
            throw new Error(`HTTP ${res.status}: ${res.statusText}`);
        }

        const usuario = await res.json();
        return usuario;

    } catch (error) {
        console.error("Falló la petición:", error.message);
        return null;
    }
}

// Llamar la función
const usuario = await cargarUsuario(1);
if (usuario) {
    console.log(usuario.name);
}
PropiedadDescripciónEjemplo
response.oktrue si status 200–299if (!res.ok) throw...
response.statusCódigo HTTP numérico200, 404, 500
response.statusTextMensaje del status"OK", "Not Found"
response.json()Parsea el body como JSONdevuelve una Promise
response.text()Body como string planodevuelve una Promise

POST — enviar datos

Para enviar datos al servidor usás el método POST y pasás el body como JSON. No olvidés el header Content-Type.

async function crearPost(titulo, cuerpo, userId) {
    try {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify({
                title:  titulo,
                body:   cuerpo,
                userId: userId,
            }),
        });

        if (!res.ok) throw new Error(`Error ${res.status}`);

        const nuevoPost = await res.json();
        console.log("Post creado con ID:", nuevoPost.id);
        return nuevoPost;

    } catch (error) {
        console.error("No se pudo crear:", error.message);
    }
}

crearPost("Mi título", "Contenido del post", 1);
JSON.stringify() vs JSON.parse()

JSON.stringify(obj) convierte un objeto JavaScript a texto JSON para enviarlo. JSON.parse(texto) hace lo inverso. response.json() llama a parse internamente.

Probá el Ejemplo 2: POST y manejo de errores

Múltiples peticiones en paralelo

Si necesitás hacer varias peticiones independientes, Promise.all() las lanza en paralelo y espera a que todas terminen — mucho más rápido que hacerlas una por una.

// ❌ Secuencial — espera una antes de empezar la otra
const usuarios  = await fetch(url1).then(r => r.json());
const posts     = await fetch(url2).then(r => r.json());
// tarda: tiempo(usuarios) + tiempo(posts)

// ✅ Paralelo — las dos peticiones van al mismo tiempo
const [usuarios, posts] = await Promise.all([
    fetch(url1).then(r => r.json()),
    fetch(url2).then(r => r.json()),
]);
// tarda: max(tiempo(usuarios), tiempo(posts))

// Promise.allSettled — continúa aunque alguna falle
const resultados = await Promise.allSettled([
    fetch(url1).then(r => r.json()),
    fetch(url2).then(r => r.json()),
    fetch(url3).then(r => r.json()),
]);

resultados.forEach((resultado, i) => {
    if (resultado.status === "fulfilled") {
        console.log(`Petición ${i}: OK`, resultado.value);
    } else {
        console.log(`Petición ${i}: Error`, resultado.reason);
    }
});
Probá el Ejemplo 3: Peticiones en paralelo

Estados de carga — loading, error, datos

Una buena UX siempre muestra feedback: un indicador mientras carga, el resultado si tuvo éxito, y un mensaje claro si hubo un error. Este patrón se repite en cualquier app que use fetch.

const contenedor = document.getElementById("contenido");

function mostrarLoading() {
    contenedor.innerHTML = `
        <div class="loading">
            <span class="spinner"></span> Cargando...
        </div>`;
}

function mostrarError(mensaje) {
    contenedor.innerHTML = `
        <div class="error">
            ❌ ${mensaje}
            <button onclick="cargar()">Reintentar</button>
        </div>`;
}

function mostrarDatos(datos) {
    contenedor.innerHTML = datos
        .map(item => `<div class="item">${item.title}</div>`)
        .join("");
}

async function cargar() {
    mostrarLoading();
    try {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
        if (!res.ok) throw new Error(`Error ${res.status}`);
        const datos = await res.json();
        mostrarDatos(datos);
    } catch (error) {
        mostrarError(error.message);
    }
}

cargar();
Probá el Ejemplo 4: App completa con fetch

Abortar una petición

Con AbortController podés cancelar una petición en curso — útil para búsquedas en tiempo real donde cada keystroke lanza una nueva petición.

let controller = null;

async function buscar(termino) {
    // Cancelar la petición anterior si todavía está en curso
    if (controller) controller.abort();
    controller = new AbortController();

    try {
        const res = await fetch(
            `https://jsonplaceholder.typicode.com/posts?q=${termino}`,
            { signal: controller.signal }
        );
        const datos = await res.json();
        mostrar(datos);
    } catch (error) {
        if (error.name === "AbortError") return; // fue cancelada — ignorar
        console.error("Error de red:", error);
    }
}

// Buscar al escribir, cancelando la petición anterior
input.addEventListener("input", (e) => {
    buscar(e.target.value);
});

Métodos HTTP más usados

MétodoAcciónTiene bodyUso típico
GETLeer datosNoCargar lista, perfil, búsqueda
POSTCrear recurso✅ SíRegistrar usuario, crear post
PUTReemplazar recurso✅ SíEditar perfil completo
PATCHModificar parcialmente✅ SíCambiar solo el email
DELETEEliminar recursoNoBorrar comentario, cuenta

Buenas Prácticas

  • Siempre verificá response.ok: fetch no lanza error en 404 o 500 — tenés que controlarlo vos
  • Siempre usá try/catch: los errores de red sí lanzan excepciones
  • async/await sobre .then(): más legible, más fácil de debuggear
  • Promise.all() para peticiones paralelas: no las hagas secuenciales si no dependen una de la otra
  • AbortController para búsquedas: evita condiciones de carrera cuando el usuario escribe rápido
  • Mostrá estados de carga y error: el usuario siempre tiene que saber qué está pasando
  • Nunca pongas API keys en el frontend: el código JS es visible para cualquiera — las keys van en el servidor