Promesas y async/await: de la teoría al código

En el día a día de cualquier proyecto frontend, la gestión de operaciones asíncronas es una constante. Desde peticiones HTTP hasta timers, JavaScript nos ofrece promesas y, a partir de ES2017, la sintaxis más legible de async/await. En este artículo vamos a desglosar su funcionamiento interno y, lo más importante, a ver cómo aplicarlos en código real.
¿Qué es una promesa?
Una promesa es, esencialmente, un contenedor que representa el resultado futuro de una operación que puede completarse con éxito (resolve) o fallar (reject). Su estado inicial es pendiente, y una vez que se resuelve pasa a cumplida o rechazada. La API mínima incluye los métodos then, catch y finally.
const promesa = new Promise((resolve, reject) => {
const exito = Math.random() > 0.5;
setTimeout(() => {
exito ? resolve('Todo bien') : reject(new Error('Algo falló'));
}, 1000);
});
promesa
.then(valor => console.log(valor))
.catch(err => console.error(err));
Cadenas de promesas
Una de las grandes ventajas es la posibilidad de encadenar operaciones. Cada then devuelve una nueva promesa, lo que permite crear flujos lineales sin caer en el temido "callback hell". Sin embargo, la legibilidad puede decaer si la cadena se vuelve larga.
fetch('/api/datos')
.then(res => res.json())
.then(datos => fetch(/api/detalle/${datos.id}))
.then(res => res.json())
.then(detalle => console.log(detalle))
.catch(err => console.error('Error en la cadena', err));
Async/Await en acción
Con async declaramos una función que siempre devuelve una promesa. Dentro de ella, await pausa la ejecución hasta que la promesa se resuelva o rechace, haciendo que el código parezca sincrónico. Esto mejora la claridad, especialmente en flujos complejos.
async function obtenerDetalle() {
try {
const res = await fetch('/api/datos');
const datos = await res.json();
const resDetalle = await fetch(/api/detalle/${datos.id});
const detalle = await resDetalle.json();
console.log(detalle);
} catch (err) {
console.error('Error en async/await', err);
}
}
obtenerDetalle();
Manejo de errores
En las promesas tradicionales, cada paso necesita su propio catch o un then con segundo argumento. Con async/await, basta con envolver el bloque completo en try / catch. Además, finally sigue disponible para limpiar recursos.
async function procesar() {
try {
const datos = await fetchDatos(); // puede lanzar
await guardarEnBD(datos);
} catch (e) {
console.warn('Algo salió mal', e);
} finally {
console.log('Operación finalizada');
}
}
Paralelismo con Promise.all
Cuando varias operaciones son independientes, podemos ejecutarlas en paralelo usando Promise.all. Con await esto sigue siendo sencillo:
async function cargarTodo() {
const [usuarios, posts] = await Promise.all([
fetch('/api/usuarios').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
console.log('Usuarios', usuarios);
console.log('Posts', posts);
}
Buenas prácticas
- Siempre devuelve una promesa: evita mezclar callbacks y promesas.
- Usa nombres claros para funciones async, por ejemplo
fetchUsuarioen vez degetData. - Limita el nivel de anidación: si una función necesita varios
await, considera extraer lógica a helpers. - Maneja errores centralmente: un
catchal final de la cadena o en el nivel superior de la aplicación.
Conclusión
Las promesas y async/await son herramientas complementarias que, bien combinadas, simplifican enormemente el código asíncrono. Entender su mecánica interna ayuda a evitar trampas comunes, como olvidar return en una cadena o no capturar excepciones. Aplica los ejemplos anteriores en tus proyectos y verás cómo tu código gana claridad y robustez.