Async/Await en JavaScript: Adiós Callback Hell

Elimina el callback hell para siempre y escribe código asíncrono limpio que realmente entiendas

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:

❌ Código Bloqueante (No existe) 📋
// ❌ 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 📋
// ❌ 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.

✅ Async/Await - Código Limpio 📋
// ✅ 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:

Regla #1: async antes de function 📋
// ✅ 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:

Regla #2: await solo con Promises 📋
// ✅ 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:

✅ Ejemplo Práctico: API del Clima 📋
// ✅ 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

❌ Error #1: Olvidar 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.ok antes 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.all cuando 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 HTTP

Cómo usar fetch correctamente, con cabeceras, autenticación y manejo de errores HTTP