Haces una llamada a una API. Ves que en el console.log de debajo aparece undefined. Llevas media hora mirando el código y no entiendes por qué. El dato está ahí, la petición funciona, pero cuando intentas usarlo no existe todavía.
Ese es el problema del código asíncrono mal entendido. JavaScript no espera a que la petición termine antes de seguir ejecutando el código que viene después. Async/await existe precisamente para escribir ese código de forma que parezca secuencial aunque no lo sea.
¿Qué vamos a ver?
- Qué es el código asíncrono y por qué da tantos problemas al principio
- Cómo funciona async/await y por qué sustituyó a los callbacks
- Manejo de errores con try/catch/finally
- Un buscador de películas real como proyecto
- Los tres errores concretos que todo el mundo comete
Qué es el código asíncrono
JavaScript es de un solo hilo: ejecuta una cosa a la vez. Si hiciera una petición a un servidor y se quedara esperando la respuesta bloqueado, tu página se congelaría varios segundos cada vez que cargara datos.
La solución es no esperar: JavaScript lanza la petición y sigue ejecutando el resto del código. Cuando el servidor responde, ejecuta el callback o resuelve la Promise. El problema es que esto cambia el orden en que se ejecutan las cosas, y ahí es donde se lía todo.
📝 Ejemplo del problema:
Sin código asíncrono, tu página se "congela" esperando respuestas:
// ❌ MAL: Esto NO existe en JavaScript (por suerte)
let datos = esperarRespuesta("https://api.ejemplo.com/datos"); // 😱 Página congelada 3 segundos
console.log(datos);
El callback hell: por qué async/await existe
Antes de async/await, la única forma de manejar código asíncrono era con callbacks: funciones que se ejecutaban cuando terminaba la operación. Funcionaba, pero en cuanto necesitabas encadenar varias operaciones dependientes entre sí, el código quedaba así:
// ❌ CALLBACK HELL - Pirámide del infierno
obtenerUsuario(1, function(usuario) {
obtenerPosts(usuario.id, function(posts) {
obtenerComentarios(posts[0].id, function(comentarios) {
console.log("Finalmente terminé!");
// 😵 Y ahora... ¿cómo manejo errores aquí?
});
});
});
❌ Problemas del callback hell:
- Difícil de leer: Parece una pirámide torcida
- Difícil de mantener: Cambiar algo rompe todo
- Manejo de errores horrible: Un try/catch por cada nivel
- Debugging imposible: Stack traces confusos
Async/Await: el mismo resultado, código legible
Async/await es azúcar sintáctico sobre las Promises: por debajo sigue siendo lo mismo, pero el código se lee de arriba a abajo como si fuera síncrono. Eso facilita mucho el debugging y el mantenimiento.
// ✅ Mismo resultado, pero se lee de arriba a abajo
async function obtenerDatosCompletos() {
try {
const usuario = await obtenerUsuario(1);
const posts = await obtenerPosts(usuario.id);
const comentarios = await obtenerComentarios(posts[0].id);
console.log("Listo. Un solo catch maneja todos los errores");
} catch (error) {
console.error("Error:", error);
}
}
Las dos reglas que necesitas recordar
Regla 1: async antes de la función
Para usar await, necesitas declarar la función como async:
// ✅ BIEN: Función async
async function miFuncion() {
const resultado = await fetch("https://api.ejemplo.com");
}
// ✅ BIEN: Arrow function async
const miFuncion = async () => {
const resultado = await fetch("https://api.ejemplo.com");
}
// ❌ MAL: await sin async
function funcionMala() {
const resultado = await fetch("https://api.ejemplo.com"); // 💥 Error!
}
Regla 2: await solo funciona con Promises
await pausa la ejecución hasta que la Promise se resuelve:
// ✅ BIEN: fetch devuelve una Promise
const respuesta = await fetch("https://api.ejemplo.com");
// ❌ MAL: Los números no son Promises
const numero = await 42; // Funciona pero no tiene sentido
Try/catch: no es opcional
Maneja errores correctamente o tu app va a explotar:
async function funcionSegura() {
try {
const respuesta = await fetch("https://api-que-puede-fallar.com");
const datos = await respuesta.json();
return datos;
} catch (error) {
console.error("Algo salió mal:", error);
return null; // Valor por defecto
}
}
💡 Consejo pro:
Siempre piensa qué debería pasar si tu API no responde. Tu código debe seguir funcionando aunque todo se rompa.
Ejemplo práctico: consultar una API del clima
Una función real que obtiene datos del clima, con manejo de errores y verificación de respuesta:
// ✅ Función completa para obtener el clima
async function obtenerClima(ciudad) {
try {
// 1. Hacer la petición HTTP
const respuesta = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${ciudad}&appid=TU_API_KEY`
);
// 2. Verificar si la respuesta es exitosa
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
// 3. Convertir respuesta a JSON
const datos = await respuesta.json();
// 4. Procesar los datos
const clima = {
ciudad: datos.name,
temperatura: Math.round(datos.main.temp - 273.15), // Kelvin a Celsius
descripcion: datos.weather[0].description
};
return clima;
} catch (error) {
console.error("Error obteniendo el clima:", error);
return null;
}
}
// Cómo usar la función:
async function mostrarClima() {
const clima = await obtenerClima("Madrid");
if (clima) {
console.log(`En ${clima.ciudad} hace ${clima.temperatura}°C y está ${clima.descripcion}`);
} else {
console.log("No se pudo obtener el clima");
}
}
Los tres errores que comete todo el mundo
Error 1: olvidar el await
// ❌ MAL: Sin await obtienes una Promise, no el resultado
async function funcionMala() {
const respuesta = fetch("https://api.ejemplo.com"); // 🤔 Esto es una Promise
console.log(respuesta); // Promise {<pending>}
}
// ✅ BIEN: Con await obtienes el resultado
async function funcionBuena() {
const respuesta = await fetch("https://api.ejemplo.com"); // Ahora sí
console.log(respuesta); // Response object
}
Error 2: no manejar errores
// ❌ MAL: Sin try/catch tu app explota
async function funcionPeligrosa() {
const datos = await fetch("https://api-que-no-existe.com"); // Explota
}
// ✅ BIEN: Manejando errores correctamente
async function funcionSegura() {
try {
const datos = await fetch("https://api-que-no-existe.com");
} catch (error) {
console.error("Tranquilo, error controlado"); // Todo bien
}
}
Error 3: hacer llamadas en serie cuando pueden ir en paralelo
// ❌ MAL: Haciendo llamadas una tras otra (lento)
async function funcionLenta() {
const usuario1 = await fetch("/api/usuario/1"); // 1 segundo
const usuario2 = await fetch("/api/usuario/2"); // +1 segundo = 2 segundos total
}
// ✅ BIEN: Llamadas en paralelo (rápido)
async function funcionRapida() {
const [usuario1, usuario2] = await Promise.all([
fetch("/api/usuario/1"),
fetch("/api/usuario/2")
]); // 1 segundo total
}
Proyecto: Buscador de películas con OMDB
Ponemos todo junto: una pequeña app que busca películas usando la API de OMDB. Hay que registrarse gratis en omdbapi.com para obtener la API key.
HTML (index.html):
<div class="buscador-peliculas">
<h1>🎬 Buscador de Películas</h1>
<div class="formulario">
<input type="text" id="busqueda" placeholder="Buscar película...">
<button id="btnBuscar">Buscar</button>
</div>
<div id="loading" class="loading oculto">
Buscando película...
</div>
<div id="resultado" class="resultado"></div>
</div>
JavaScript (app.js):
// Función principal: buscar película usando async/await
async function buscarPelicula(titulo) {
const API_KEY = "tu-api-key-aqui"; // Registrarse en omdbapi.com
const url = `https://www.omdbapi.com/?t=${encodeURIComponent(titulo)}&apikey=${API_KEY}`;
try {
// 1. Mostrar loading
mostrarLoading(true);
// 2. Hacer petición a la API
const respuesta = await fetch(url);
// 3. Verificar si la petición fue exitosa
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
// 4. Convertir respuesta a JSON
const datos = await respuesta.json();
// 5. Verificar si se encontró la película
if (datos.Response === "False") {
mostrarError("Película no encontrada ");
return;
}
// 6. Mostrar resultado exitoso
mostrarPelicula(datos);
} catch (error) {
console.error("Error buscando película:", error);
mostrarError("Error de conexión. Intenta de nuevo.");
} finally {
// 7. Ocultar loading siempre (éxito o error)
mostrarLoading(false);
}
}
// Funciones auxiliares para UI
function mostrarLoading(mostrar) {
const loading = document.getElementById('loading');
loading.classList.toggle('oculto', !mostrar);
}
function mostrarPelicula(pelicula) {
const resultado = document.getElementById('resultado');
// Crear elementos del DOM dinámicamente
const tarjeta = document.createElement('div');
tarjeta.className = 'pelicula-tarjeta';
const titulo = document.createElement('h2');
titulo.textContent = pelicula.Title;
const año = document.createElement('p');
año.textContent = `Año: ${pelicula.Year}`;
const director = document.createElement('p');
director.textContent = `Director: ${pelicula.Director}`;
tarjeta.appendChild(titulo);
tarjeta.appendChild(año);
tarjeta.appendChild(director);
resultado.replaceChildren(tarjeta);
}
function mostrarError(mensaje) {
const resultado = document.getElementById('resultado');
const error = document.createElement('div');
error.className = 'error';
error.textContent = mensaje;
resultado.replaceChildren(error);
}
// Event listeners
document.addEventListener('DOMContentLoaded', function() {
const btnBuscar = document.getElementById('btnBuscar');
const inputBusqueda = document.getElementById('busqueda');
// Buscar al hacer clic
btnBuscar.addEventListener('click', function() {
const titulo = inputBusqueda.value.trim();
if (titulo) {
buscarPelicula(titulo); // Aquí se usa async/await
}
});
// Buscar al presionar Enter
inputBusqueda.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
btnBuscar.click();
}
});
});
Buenas prácticas que marcan la diferencia
Hazlo SIEMPRE:
- Usa try/catch: Nunca asumas que tu API va a funcionar
- Verifica respuestas: Checkea
response.okantes de hacer.json() - Da feedback: Muestra loading states al usuario
- Valida inputs: No envíes datos vacíos o malformados
NUNCA hagas esto:
- Await sin async: Te dará error de sintaxis
- Fetch sin verificar:
response.json()puede fallar - Llamadas secuenciales: Usa
Promise.allcuando sea posible - Ignorar errores: Tu app va a explotar en producción
Async/await vs Promises vs Callbacks
Los tres métodos resuelven el mismo problema. La diferencia es solo de legibilidad. Aquí el mismo código con cada uno:
La misma operación con cada método:
// 1. CALLBACKS (el infierno) ❌
obtenerUsuario(1, function(usuario) {
obtenerPosts(usuario.id, function(posts) {
console.log("Posts obtenidos", posts);
});
});
// 2. PROMISES (mejor pero verboso)
obtenerUsuario(1)
.then(usuario => obtenerPosts(usuario.id))
.then(posts => console.log("Posts obtenidos", posts))
.catch(error => console.error(error));
// 3. ASYNC/AWAIT (perfecto)
async function obtenerDatos() {
try {
const usuario = await obtenerUsuario(1);
const posts = await obtenerPosts(usuario.id);
console.log("Posts obtenidos", posts);
} catch (error) {
console.error(error);
}
}
Tres patrones que usarás constantemente
Promise.all para llamadas en paralelo
// ❌ MAL: Secuencial (lento)
const usuario = await fetch('/usuario/1');
const posts = await fetch('/posts');
//BIEN: Paralelo (rápido)
const [usuario, posts] = await Promise.all([
fetch('/usuario/1'),
fetch('/posts')
]);
Finally para limpiar estado
async function enviarFormulario() {
try {
mostrarLoader(true);
const resultado = await enviarDatos();
mostrarExito(resultado);
} catch (error) {
mostrarError(error);
} finally {
// Se ejecuta SIEMPRE (éxito o error)
mostrarLoader(false);
habilitarBoton(true);
}
}
Timeouts para APIs que no responden
async function fetchConTimeout(url, timeout = 5000) {
const controller = new AbortController();
// Cancelar después del timeout
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const respuesta = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId); // Cancelar timeout si terminó
return respuesta;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
Por dónde seguir
El siguiente paso natural es usar async/await con la Fetch API en un proyecto real. Ahí es donde aparecen los problemas de verdad: CORS, APIs que devuelven errores dentro del JSON con status 200, o tokens de autenticación en las cabeceras.
Siguiente lectura recomendada
Si quieres practicar async/await con una API real desde cero:
JavaScript Fetch API: tu primera llamada HTTPCómo usar fetch correctamente, con cabeceras, autenticación y manejo de errores HTTP