Si llevas un tiempo aprendiendo JavaScript, en algún momento te has topado con código que parece extraño: funciones que devuelven algo llamado Promise, palabras clave como then, catch, async y await que aparecen en todas partes. ¿Qué significan? ¿Por qué existen?
En este artículo vas a entender desde cero qué es la programación asíncrona, qué problema resuelven las Promesas y cómo usar async/await para escribir código más limpio y fácil de leer.
El problema: JavaScript es de un solo hilo
JavaScript fue diseñado para ejecutarse en el navegador, y tiene una característica fundamental: solo puede hacer una cosa a la vez. A esto se le llama ser single-threaded, de un solo hilo de ejecución.
Esto plantea un problema inmediato. ¿Qué pasa cuando necesitas hacer algo que tarda tiempo, como:
- Pedir datos a un servidor externo (una API).
- Leer un archivo del disco.
- Esperar la respuesta de una base de datos.
- Cargar una imagen desde internet.
Si JavaScript tuviera que esperar bloqueado mientras esas operaciones terminan, la página web quedaría congelada. Nada respondería: ni los botones, ni el scroll, ni ninguna interacción del usuario.
La solución a este problema es la programación asíncrona: delegar esas tareas lentas para que ocurran en segundo plano y continuar ejecutando el resto del código mientras tanto.
El punto de partida: los callbacks
Antes de que existieran las Promesas, JavaScript resolvía la asincronía con callbacks. Un callback es una función que le pasas a otra función para que la ejecute cuando termine su tarea.
function obtenerDatos(callback) {
setTimeout(function () {
callback("Datos recibidos");
}, 2000);
}
obtenerDatos(function (resultado) {
console.log(resultado); // "Datos recibidos" (después de 2 segundos)
});Esto funciona bien para casos sencillos. El problema aparece cuando necesitas encadenar varias operaciones asíncronas una tras otra. El código se vuelve profundamente anidado y difícil de leer, un fenómeno conocido como callback hell o el infierno de los callbacks:
obtenerUsuario(function (usuario) {
obtenerPedidos(usuario.id, function (pedidos) {
obtenerDetalle(pedidos[0].id, function (detalle) {
obtenerFactura(detalle.id, function (factura) {
console.log(factura);
// Y así podría seguir...
});
});
});
});Este código es difícil de mantener, difícil de depurar y muy fácil de romper. Las Promesas nacieron para resolver exactamente este problema.
¿Qué es una Promesa?
Una Promesa (Promise) es un objeto de JavaScript que representa el resultado eventual de una operación asíncrona. Funciona como una garantía: "en algún momento en el futuro, esta operación va a terminar, y cuando lo haga, te avisaré si salió bien o mal".
Una Promesa puede estar en uno de tres estados:
- Pending (pendiente): la operación aún no ha terminado.
- Fulfilled (cumplida): la operación terminó con éxito y tiene un valor resultante.
- Rejected (rechazada): la operación falló y tiene un motivo de error.
Una vez que una Promesa pasa de pending a cualquiera de los otros dos estados, no puede cambiar. Es inmutable.
Cómo crear una Promesa
Se crea con el constructor new Promise(), que recibe una función con dos parámetros: resolve y reject. Llamas a resolve cuando todo sale bien, y a reject cuando ocurre un error.
const miPromesa = new Promise(function (resolve, reject) {
const exito = true;
if (exito) {
resolve("La operación fue exitosa");
} else {
reject("Algo salió mal");
}
});Con funciones de flecha, que es la forma más común de verlo:
const miPromesa = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Datos listos después de 2 segundos");
}, 2000);
});Cómo consumir una Promesa con then y catch
Para usar el resultado de una Promesa, se encadenan los métodos .then() y .catch().
.then()se ejecuta cuando la Promesa se cumple (resolve)..catch()se ejecuta cuando la Promesa es rechazada (reject)..finally()se ejecuta siempre, sin importar el resultado.
miPromesa
.then((resultado) => {
console.log(resultado); // "Datos listos después de 2 segundos"
})
.catch((error) => {
console.error("Error:", error);
})
.finally(() => {
console.log("La operación terminó (con éxito o con error)");
});Encadenamiento de Promesas
Una de las grandes ventajas de las Promesas sobre los callbacks es que se pueden encadenar de forma legible. Cada .then() puede devolver una nueva Promesa o un valor, que pasa al siguiente .then().
fetch("https://api.ejemplo.com/usuario/1")
.then((respuesta) => respuesta.json())
.then((usuario) => {
console.log("Usuario:", usuario.nombre);
return fetch(`https://api.ejemplo.com/pedidos/${usuario.id}`);
})
.then((respuesta) => respuesta.json())
.then((pedidos) => {
console.log("Pedidos:", pedidos);
})
.catch((error) => {
console.error("Ocurrió un error:", error);
});Comparado con el callback hell de antes, este código ya es mucho más legible. Pero podemos hacerlo aún mejor con async/await.
Qué es async/await
async/await es una sintaxis que llegó con ES2017 y que permite escribir código asíncrono como si fuera código sincrónico. Por debajo sigue usando Promesas, pero la forma de escribirlo es mucho más natural y fácil de entender.
La palabra clave async
Cuando colocas async delante de una función, esa función automáticamente devuelve una Promesa, aunque no lo indiques explícitamente.
async function saludar() {
return "Hola";
}
saludar().then((mensaje) => console.log(mensaje)); // "Hola"La palabra clave await
await solo puede usarse dentro de una función async. Le dice a JavaScript: "espera aquí hasta que esta Promesa se resuelva, y luego continúa con el resultado".
async function obtenerDatos() {
const respuesta = await fetch("https://api.ejemplo.com/datos");
const datos = await respuesta.json();
console.log(datos);
}
obtenerDatos();El código parece sincrónico y lineal, pero por debajo JavaScript lo maneja de forma asíncrona sin bloquear el hilo principal.
Manejo de errores con async/await
Con async/await, el manejo de errores se hace con bloques try/catch, exactamente igual que con código sincrónico normal. Esto es mucho más intuitivo que encadenar .catch().
async function obtenerUsuario(id) {
try {
const respuesta = await fetch(`https://api.ejemplo.com/usuario/${id}`);
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
const usuario = await respuesta.json();
console.log("Usuario:", usuario);
return usuario;
} catch (error) {
console.error("No se pudo obtener el usuario:", error.message);
}
}
obtenerUsuario(1);Ejemplo completo: comparando los tres enfoques
Para que veas claramente la evolución, aquí está el mismo código escrito con callbacks, con Promesas y con async/await:
Con callbacks
function obtenerPost(id, callback) {
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then((res) => res.json())
.then((data) => callback(null, data))
.catch((err) => callback(err, null));
}
obtenerPost(1, function (error, post) {
if (error) {
console.error("Error:", error);
return;
}
console.log("Título:", post.title);
});Con Promesas
function obtenerPost(id) {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then((res) => res.json());
}
obtenerPost(1)
.then((post) => console.log("Título:", post.title))
.catch((error) => console.error("Error:", error));Con async/await
async function obtenerPost(id) {
try {
const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await respuesta.json();
console.log("Título:", post.title);
} catch (error) {
console.error("Error:", error);
}
}
obtenerPost(1);Los tres hacen exactamente lo mismo. Pero el tercero es el más legible, el más fácil de mantener y el que más se parece al código que encontrarás en proyectos reales hoy en día.
Ejecutar varias Promesas al mismo tiempo
Hay situaciones donde necesitas ejecutar varias operaciones asíncronas en paralelo y esperar a que todas terminen. Para eso existe Promise.all().
async function obtenerVariosUsuarios() {
try {
const [usuario1, usuario2, usuario3] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/users/1").then((r) => r.json()),
fetch("https://jsonplaceholder.typicode.com/users/2").then((r) => r.json()),
fetch("https://jsonplaceholder.typicode.com/users/3").then((r) => r.json()),
]);
console.log(usuario1.name);
console.log(usuario2.name);
console.log(usuario3.name);
} catch (error) {
console.error("Alguna petición falló:", error);
}
}
obtenerVariosUsuarios();Promise.all() lanza todas las peticiones al mismo tiempo y espera a que todas terminen. Si cualquiera de ellas falla, el catch captura el error. Esto es mucho más eficiente que esperar las peticiones una por una.
Otras variantes útiles
- Promise.allSettled(): espera a que todas terminen, sin importar si alguna falló. Te devuelve el resultado de cada una individualmente.
- Promise.race(): devuelve el resultado de la primera Promesa que termine, ya sea con éxito o con error.
- Promise.any(): devuelve el resultado de la primera Promesa que se cumpla con éxito, ignorando las rechazadas.
// Promise.allSettled: no falla aunque alguna sea rechazada
const resultados = await Promise.allSettled([
fetch("https://api.ejemplo.com/datos1").then((r) => r.json()),
fetch("https://api.ejemplo.com/datos2").then((r) => r.json()),
]);
resultados.forEach((resultado) => {
if (resultado.status === "fulfilled") {
console.log("Éxito:", resultado.value);
} else {
console.log("Error:", resultado.reason);
}
});Errores comunes al usar async/await
Olvidar el await
Si olvidas poner await delante de una Promesa, recibirás el objeto Promise en lugar del valor resuelto.
// ❌ Incorrecto: datos es una Promise, no el objeto JSON
async function obtenerDatos() {
const respuesta = await fetch("https://api.ejemplo.com/datos");
const datos = respuesta.json(); // falta await
console.log(datos); // Promise { }
}
// ✅ Correcto
async function obtenerDatos() {
const respuesta = await fetch("https://api.ejemplo.com/datos");
const datos = await respuesta.json();
console.log(datos); // { ... objeto con los datos ... }
}Usar await fuera de una función async
await solo funciona dentro de funciones marcadas con async. Usarlo fuera genera un error de sintaxis.
// ❌ Incorrecto: await fuera de async
const datos = await fetch("https://api.ejemplo.com/datos"); // SyntaxError
// ✅ Correcto: siempre dentro de async
async function cargar() {
const datos = await fetch("https://api.ejemplo.com/datos");
}No manejar los errores
Si una Promesa es rechazada y no hay un catch o un bloque try/catch, obtendrás un error no controlado que puede romper tu aplicación silenciosamente.
// ❌ Sin manejo de errores
async function cargar() {
const datos = await fetch("https://api-que-no-existe.com/datos");
// Si falla, el error no se captura
}
// ✅ Con manejo de errores
async function cargar() {
try {
const datos = await fetch("https://api-que-no-existe.com/datos");
} catch (error) {
console.error("Error al cargar:", error.message);
}
}Cuándo usar Promesas y cuándo async/await
En la mayoría de los casos, async/await es la opción preferida porque hace el código más legible. Sin embargo, hay situaciones donde las Promesas con .then() tienen más sentido:
- Cuando necesitas encadenar transformaciones simples en una sola línea.
- Cuando trabajas con
Promise.all()oPromise.race()dentro de una expresión. - En callbacks de métodos como
.map()o.filter()donde no puedes usarasyncdirectamente.
Lo más importante es que no son enfoques opuestos: se complementan. Puedes mezclarlos en el mismo proyecto según lo que haga el código más claro.
Dónde seguir practicando
La mejor forma de afianzar estos conceptos es usarlos en proyectos reales. Empieza con algo sencillo: un proyecto que consuma una API pública y muestre los datos en pantalla. Puedes usar APIs gratuitas como JSONPlaceholder, OpenWeatherMap o la de GitHub para practicar.
Si aún estás consolidando tu base en JavaScript, te recomendamos leer primero nuestra guía sobre qué es JavaScript y para qué se usa, y también el artículo sobre qué es el DOM y cómo manipularlo. Combinados con lo que aprendiste aquí, tendrás una base sólida para construir aplicaciones web modernas.
Conclusión
Las Promesas y async/await son la solución de JavaScript al problema de la programación asíncrona. Las Promesas representan valores que estarán disponibles en el futuro, y async/await es la forma más moderna y legible de trabajar con ellas.
Recuerda los puntos clave: una Promesa tiene tres estados (pending, fulfilled, rejected), se consume con .then() y .catch(), y con async/await puedes escribir ese mismo código de forma lineal usando try/catch para los errores. Para tareas en paralelo, Promise.all() es tu mejor aliado.
Con estos conceptos dominados, estarás preparado para trabajar con APIs, bases de datos, autenticación y prácticamente cualquier operación del mundo real en JavaScript.
No hay comentarios todavía. Sé el primero en compartir tu opinión.