Saltar al contenido
schedule 15 min JavaScript

Fetch y async/await

Abre Instagram, Twitter o YouTube. Cada vez que haces scroll, aparecen nuevos posts sin recargar la página. Cuando buscas un producto en Amazon, los resultados llegan del servidor en milisegundos. Tu navegador está constantemente hablando con servidores, pidiendo datos y enviando información. Eso es exactamente lo que vas a aprender aquí: cómo hacer que tu JavaScript se comunique con el mundo exterior.

HTTP en 2 minutos: GET y POST

Cuando tu navegador pide datos a un servidor, usa el protocolo HTTP. Solo necesitas entender dos métodos por ahora:

  • GET: pedir datos. "Dame la lista de productos", "Dame los datos de este usuario".
  • POST: enviar datos. "Aquí tienes los datos del formulario de registro", "Crea este nuevo comentario".

Cada petición HTTP recibe una respuesta con un código de estado: 200 significa "todo bien", 404 es "no encontrado", 500 es "error del servidor". Seguro que el 404 te suena.

JSON: el idioma universal de los datos

Cuando un servidor te envía datos, casi siempre lo hace en formato JSON (JavaScript Object Notation). Es básicamente un objeto de JavaScript en formato texto:

app.js
// Esto es JSON (texto plano que el servidor envía)
const jsonTexto = '{"nombre": "Pikachu", "tipo": "eléctrico", "nivel": 25}';

// JSON.parse: convierte texto JSON → objeto JavaScript
const pokemon = JSON.parse(jsonTexto);
console.log(pokemon.nombre); // "Pikachu"
console.log(pokemon.nivel);  // 25

// JSON.stringify: convierte objeto JavaScript → texto JSON
const usuario = { nombre: 'Ana', edad: 28, premium: true };
const usuarioJSON = JSON.stringify(usuario);
console.log(usuarioJSON); // '{"nombre":"Ana","edad":28,"premium":true}'

// ¿Cuándo usas cada uno?
// JSON.parse     → cuando recibes datos del servidor
// JSON.stringify  → cuando envías datos al servidor

La API fetch: tu herramienta para hacer peticiones

fetch es la función nativa del navegador para hacer peticiones HTTP. Es simple, potente y no necesitas instalar nada:

app.js
// La petición más simple posible: un GET
fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
    .then((response) => response.json())  // Convertir la respuesta a JSON
    .then((data) => {
        console.log(data.name);           // "pikachu"
        console.log(data.height);         // 4
        console.log(data.types[0].type.name); // "electric"
    });

// Pero espera... ¿qué es ese .then()? Es la sintaxis de Promises.
// Funciona, pero hay una forma mucho más legible: async/await.

Promesas: la versión breve

Una Promise (promesa) es un valor que todavía no existe, pero existirá en el futuro. Cuando llamas a fetch(), no obtienes los datos inmediatamente (el servidor tarda en responder). Obtienes una promesa: "te prometo que te daré los datos cuando lleguen".

app.js
// fetch() devuelve una Promise
const promesa = fetch('https://pokeapi.co/api/v2/pokemon/pikachu');
console.log(promesa); // Promise { <pending> } — aún no hay datos

// Una Promise puede estar en 3 estados:
// 1. pending   → esperando respuesta
// 2. fulfilled → los datos llegaron correctamente
// 3. rejected  → algo salió mal (error de red, servidor caído, etc.)

// Con .then() manejas el éxito, con .catch() los errores:
fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
    .then((response) => response.json())
    .then((data) => console.log(data.name))
    .catch((error) => console.error('Error:', error));

// Pero encadenar .then() se vuelve difícil de leer rápido.
// Por eso existe async/await...

async/await: código asíncrono que se lee como síncrono

async/await es azúcar sintáctica sobre las Promises. Hace exactamente lo mismo, pero se lee muchísimo mejor:

app.js
// Marca la función como "async" para poder usar "await" dentro
const buscarPokemon = async (nombre) => {
    const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${nombre}`);
    const data = await response.json();

    console.log(`${data.name} — Tipo: ${data.types[0].type.name}`);
    console.log(`Altura: ${data.height / 10}m, Peso: ${data.weight / 10}kg`);

    return data;
};

buscarPokemon('charizard');
// "charizard — Tipo: fire"
// "Altura: 1.7m, Peso: 90.5kg"

// ¿Qué hace await?
// Pausa la ejecución de la función hasta que la Promise se resuelva.
// El código se lee de arriba a abajo, como si fuera síncrono.
// Pero NO bloquea el navegador — solo pausa esa función.

try/catch: manejar errores como un profesional

Las peticiones de red pueden fallar: el usuario pierde la conexión, el servidor está caído, la URL es incorrecta. Siempre envuelve tus peticiones en try/catch:

app.js
const buscarPokemonSeguro = async (nombre) => {
    try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${nombre}`);

        // fetch NO lanza error en 404 — solo en errores de red
        // Necesitas comprobar response.ok manualmente
        if (!response.ok) {
            throw new Error(`Pokemon no encontrado (${response.status})`);
        }

        const data = await response.json();
        return data;

    } catch (error) {
        console.error('Error al buscar Pokemon:', error.message);
        return null;
    }
};

// Uso
const pokemon = await buscarPokemonSeguro('pikachu');   // Funciona
const fake = await buscarPokemonSeguro('inventado123'); // Error controlado, devuelve null

Punto clave: fetch solo lanza un error si la petición falla por completo (sin internet, DNS no resuelve). Un 404 o 500 del servidor NO es un error de fetch — la respuesta llegó, pero con un código de error. Por eso compruebas response.ok.

Ejemplo práctico: buscador de Pokémon

Vamos a construir un buscador real que consulta la PokéAPI:

index.html
<div class="pokedex">
    <h2>Pokédex</h2>
    <form id="pokemon-form">
        <input type="text" id="pokemon-input" placeholder="Nombre del Pokémon (ej: pikachu)">
        <button type="submit">Buscar</button>
    </form>
    <div id="pokemon-resultado"></div>
</div>
app.js
const pokemonForm = document.querySelector('#pokemon-form');
const pokemonInput = document.querySelector('#pokemon-input');
const pokemonResultado = document.querySelector('#pokemon-resultado');

pokemonForm.addEventListener('submit', async (event) => {
    event.preventDefault();

    const nombre = pokemonInput.value.trim().toLowerCase();
    if (!nombre) return;

    // Estado de carga — feedback visual para el usuario
    pokemonResultado.innerHTML = '<p class="cargando">Buscando...</p>';

    try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${nombre}`);

        if (!response.ok) {
            pokemonResultado.innerHTML = `
                <p class="error">No se encontró el Pokémon "${nombre}". ¿Seguro que está bien escrito?</p>
            `;
            return;
        }

        const data = await response.json();

        // Extraer datos relevantes
        const tipos = data.types.map((t) => t.type.name).join(', ');
        const imagen = data.sprites.other['official-artwork'].front_default;
        const stats = data.stats.map((s) => `
            <li><strong>${s.stat.name}:</strong> ${s.base_stat}</li>
        `).join('');

        pokemonResultado.innerHTML = `
            <div class="pokemon-card">
                <img src="${imagen}" alt="${data.name}" width="200">
                <h3>${data.name} #${data.id}</h3>
                <p>Tipo: ${tipos}</p>
                <p>Altura: ${data.height / 10}m — Peso: ${data.weight / 10}kg</p>
                <ul class="pokemon-stats">${stats}</ul>
            </div>
        `;

    } catch (error) {
        pokemonResultado.innerHTML = `
            <p class="error">Error de conexión. Comprueba tu internet e inténtalo de nuevo.</p>
        `;
    }
});

Ejemplo: consultar el clima con Open-Meteo

Open-Meteo es una API de clima gratuita que no requiere clave de acceso. Perfecta para practicar:

app.js
// Obtener el clima actual de Madrid (latitud: 40.42, longitud: -3.70)
const obtenerClima = async (latitud, longitud) => {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitud}&longitude=${longitud}&current=temperature_2m,wind_speed_10m,weather_code`;

    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error('No se pudo obtener el clima');
        }

        const data = await response.json();
        const actual = data.current;

        return {
            temperatura: actual.temperature_2m,
            viento: actual.wind_speed_10m,
            codigo: actual.weather_code,
        };

    } catch (error) {
        console.error('Error al obtener el clima:', error.message);
        return null;
    }
};

// Uso
const climaMadrid = await obtenerClima(40.42, -3.70);
if (climaMadrid) {
    console.log(`Madrid: ${climaMadrid.temperatura}°C, viento: ${climaMadrid.viento} km/h`);
}

Promise.allSettled: múltiples peticiones en paralelo

Si necesitas hacer varias peticiones a la vez, no las hagas una detrás de otra. Usa Promise.allSettled() para lanzarlas todas en paralelo y esperar a que terminen:

app.js
// Buscar 3 Pokémon a la vez (en vez de uno detrás de otro)
const buscarVariosPokemon = async (nombres) => {
    const promesas = nombres.map((nombre) =>
        fetch(`https://pokeapi.co/api/v2/pokemon/${nombre}`)
            .then((res) => res.ok ? res.json() : null)
    );

    // allSettled espera a que TODAS terminen (aunque alguna falle)
    const resultados = await Promise.allSettled(promesas);

    resultados.forEach((resultado) => {
        if (resultado.status === 'fulfilled' && resultado.value) {
            console.log(`✓ ${resultado.value.name}`);
        } else {
            console.log('✗ No se pudo cargar un Pokémon');
        }
    });

    return resultados;
};

buscarVariosPokemon(['pikachu', 'charizard', 'inventado123']);
// ✓ pikachu
// ✓ charizard
// ✗ No se pudo cargar un Pokémon

// ¿Por qué allSettled y no Promise.all?
// - Promise.all: si UNA falla, TODAS fallan (se va al catch)
// - Promise.allSettled: espera a todas, y te dice cuáles funcionaron y cuáles no
// Generalmente allSettled es más útil en el mundo real

Promise.any: la más rápida gana

Promise.any() devuelve el resultado de la primera promesa que se resuelva con éxito. Útil cuando tienes múltiples fuentes para el mismo dato:

app.js
// Intentar obtener datos del servidor más rápido
const obtenerDatosRapido = async () => {
    try {
        const resultado = await Promise.any([
            fetch('https://api-servidor-1.ejemplo.com/datos').then((r) => r.json()),
            fetch('https://api-servidor-2.ejemplo.com/datos').then((r) => r.json()),
            fetch('https://api-servidor-3.ejemplo.com/datos').then((r) => r.json()),
        ]);

        // resultado contiene los datos del servidor que respondió primero
        console.log('Datos recibidos del servidor más rápido:', resultado);
        return resultado;

    } catch (error) {
        // Solo llega aquí si TODAS las promesas fallan
        console.error('Todos los servidores fallaron');
        return null;
    }
};

Estados de carga: la experiencia del usuario

Mientras el navegador espera la respuesta del servidor, el usuario no debería ver una pantalla en blanco. Siempre muestra un estado de carga:

app.js
const cargarProductos = async () => {
    const contenedor = document.querySelector('#productos');
    const btnCargar = document.querySelector('#btn-cargar');

    // 1. Mostrar estado de carga
    contenedor.innerHTML = '<p class="cargando">Cargando productos...</p>';
    btnCargar.disabled = true;
    btnCargar.textContent = 'Cargando...';

    try {
        const response = await fetch('https://fakestoreapi.com/products?limit=5');

        if (!response.ok) {
            throw new Error('Error al cargar productos');
        }

        const productos = await response.json();

        // 2. Mostrar los datos
        contenedor.innerHTML = productos.map((p) => `
            <div class="producto">
                <img src="${p.image}" alt="${p.title}" width="80">
                <h3>${p.title}</h3>
                <p>${p.price} €</p>
            </div>
        `).join('');

    } catch (error) {
        // 3. Mostrar error amigable
        contenedor.innerHTML = `
            <p class="error">
                No pudimos cargar los productos.
                <button onclick="cargarProductos()">Reintentar</button>
            </p>
        `;
    } finally {
        // 4. Restaurar el botón siempre (éxito o error)
        btnCargar.disabled = false;
        btnCargar.textContent = 'Cargar productos';
    }
};

Peticiones POST: enviar datos al servidor

Hasta ahora solo hemos pedido datos (GET). Para enviar datos (crear un usuario, publicar un comentario, enviar un formulario), usas POST:

app.js
// Enviar datos con POST
const crearComentario = async (comentario) => {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/comments', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json', // Le dices al servidor que envías JSON
            },
            body: JSON.stringify({
                postId: 1,
                name: comentario.nombre,
                email: comentario.email,
                body: comentario.texto,
            }),
        });

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

        const nuevoComentario = await response.json();
        console.log('Comentario creado:', nuevoComentario);
        return nuevoComentario;

    } catch (error) {
        console.error('Error al crear comentario:', error.message);
        return null;
    }
};

// Uso
crearComentario({
    nombre: 'Laura García',
    email: '[email protected]',
    texto: 'Me encanta este artículo, muy bien explicado.',
});

// La estructura de un fetch POST:
// fetch(url, {
//     method: 'POST',
//     headers: { 'Content-Type': 'application/json' },
//     body: JSON.stringify(datos),
// })

Top-level await

En módulos ES (archivos con type="module"), puedes usar await directamente sin necesidad de una función async:

index.html
<!-- El type="module" es necesario para top-level await -->
<script type="module" src="app.js"></script>
app.js
// En un módulo ES, puedes hacer esto directamente
const response = await fetch('https://pokeapi.co/api/v2/pokemon/eevee');
const eevee = await response.json();

console.log(eevee.name); // "eevee"

// Sin necesidad de envolver todo en una función async.
// Esto solo funciona en módulos (type="module"), no en scripts normales.

Patrón completo: petición con carga, error y datos

Este es el patrón que usarás en el 90% de los casos. Memorízalo:

app.js
const cargarDatos = async (url) => {
    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const data = await response.json();
        return { data, error: null };

    } catch (error) {
        return { data: null, error: error.message };
    }
};

// Uso limpio con desestructuración
const { data, error } = await cargarDatos('https://pokeapi.co/api/v2/pokemon/mew');

if (error) {
    console.error(error);
} else {
    console.log(data.name); // "mew"
}
code

Construye un buscador de Pokémon con la PokéAPI

Medio schedule 25 min

Crea una página que consulte la PokéAPI con estos requisitos:

  • Un campo de texto para escribir el nombre del Pokémon y un botón de buscar
  • Al buscar, muestra un estado de carga: "Buscando..." con el botón deshabilitado
  • Si se encuentra, muestra: imagen oficial, nombre, tipos, altura, peso y al menos 3 stats (hp, attack, defense)
  • Si no se encuentra (404), muestra un mensaje de error amigable
  • Si hay un error de red, muestra un mensaje diferente
  • Usa async/await con try/catch
  • Bonus: añade un botón "Pokémon aleatorio" que genere un número entre 1 y 1010 y busque ese ID
  • Bonus: usa Promise.allSettled para cargar un "equipo" de 3 Pokémon a la vez

URL base: https://pokeapi.co/api/v2/pokemon/{nombre-o-id}

lightbulb Pistas

La imagen oficial está en data.sprites.other['official-artwork'].front_default. Los tipos están en data.types (es un array, usa .map()). Para el Pokémon aleatorio, usa Math.floor(Math.random() * 1010) + 1. Para el equipo, crea un array de 3 fetch() y pásalo a Promise.allSettled().

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.