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:
// 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:
// 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".
// 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:
// 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:
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:
fetchsolo 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 compruebasresponse.ok.
Ejemplo práctico: buscador de Pokémon
Vamos a construir un buscador real que consulta la PokéAPI:
<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>
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:
// 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}¤t=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:
// 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:
// 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:
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:
// 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:
<!-- El type="module" es necesario para top-level await -->
<script type="module" src="app.js"></script>
// 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:
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"
}
Construye un buscador de Pokémon con la PokéAPI
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/awaitcontry/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.allSettledpara 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().