Menu

Promesas en JavaScript: then, catch y Promise.all

Cómo funcionan las promesas en JavaScript: sus tres estados, encadenar con then y catch, combinar varias con Promise.all y crear las tuyas con new Promise.

Una promesa es un marcador de posición para un valor futuro

Cuando JavaScript tiene que hacer algo que lleva tiempo —una petición de red, leer un archivo o esperar un temporizador—, no puede devolverte el resultado al instante. Lo que te devuelve es una promesa: un objeto que representa un valor que va a existir en algún momento.

index.js
Output
Click Run to see the output here.

El primer console.log muestra una Promise en estado pending. Medio segundo después, la Promise se resuelve y el callback del .then se ejecuta con el valor. La Promise en sí no es más que un objeto; lo interesante es que sabe cómo avisar a quien esté escuchando en cuanto tiene el valor listo.

Los tres estados de una promesa

Toda promesa en JavaScript se encuentra siempre en uno de estos tres estados:

  • pending (pendiente) — el trabajo está en curso. Todavía no hay valor.
  • fulfilled (cumplida) — el trabajo terminó con éxito. Ya hay un valor disponible.
  • rejected (rechazada) — el trabajo falló. Hay un error disponible.

Una Promise pasa de pending a fulfilled o a rejected una sola vez, y a partir de ahí se queda así para siempre. No se puede "des-resolver" una Promise ni resolverla dos veces.

index.js
Output
Click Run to see the output here.

Promise.resolve(value) crea una promesa ya resuelta; Promise.reject(error), una ya rechazada. Vienen muy bien para tests y para devolver una promesa desde una función que, a veces, ya tiene la respuesta al instante.

Leer el valor: .then y .catch

A una promesa no se le saca el valor directamente: le pasas un callback a .then y la propia promesa lo invoca en cuanto el valor está listo:

index.js
Output
Click Run to see the output here.

.catch(fn) se ejecuta cuando la Promise se rechaza. Por dentro, es solo un atajo para .then(undefined, fn). Un .catch() al final de la cadena atrapa los rechazos de cualquier paso anterior, así que no hace falta poner uno después de cada .then.

Encadenar promesas en JavaScript: cada .then devuelve una nueva Promise

Aquí es donde mucha gente se pierde. .then() no se limita a ejecutar un callback: devuelve una nueva Promise que se resuelve con lo que haya retornado ese callback. Gracias a eso puedes encadenar:

index.js
Output
Click Run to see the output here.

Cada paso alimenta al siguiente. Si un callback de .then devuelve una Promise, la cadena espera a esa Promise antes de continuar, de modo que los pasos asíncronos se componen sin complicaciones:

index.js
Output
Click Run to see the output here.

Tres pasos asíncronos en secuencia, sin anidar nada. Compara esto con la misma lógica escrita con callbacks y entenderás por qué las promesas se volvieron tan populares.

Los errores caen en cascada por la cadena

Cuando una promesa se rechaza, se saltan todos los .then hasta encontrar un .catch. Ese es, básicamente, todo el modelo de manejo de errores con promesas:

index.js
Output
Click Run to see the output here.

Si lanzas un error dentro de un .then, la Promise que devuelve ese .then queda rechazada. El siguiente .then ve el rechazo y lo va propagando hasta que un .catch lo atrapa. Con un único .catch al final de la cadena suele bastar, y si no pones ninguno vas a ver el típico aviso de "unhandled promise rejection", que conviene arreglar cuanto antes.

Crear tus propias promesas con new Promise

La mayoría de las veces vas a consumir promesas que ya te dan las librerías. Pero de vez en cuando toca envolver algo que no devuelve una, normalmente una API antigua basada en callbacks:

index.js
Output
Click Run to see the output here.

La función que le pasas a new Promise se llama el executor. Recibe dos argumentos: resolve (llámalo con el valor de éxito) y reject (llámalo con un error). Debes invocar uno de los dos exactamente una vez. A partir de ahí, las llamadas posteriores se ignoran.

Dos costumbres que te ahorran dolores de cabeza:

  • Usa new Promise únicamente cuando necesites envolver algo que todavía no esté basado en promesas. Si una función ya devuelve una Promise, simplemente retórnala.
  • Rechaza siempre con un objeto Error, nunca con un string. Los stack traces merecen conservarse.

Ejecutar tareas en paralelo con Promise.all

Las cadenas de .then se ejecutan de forma secuencial. Cuando tienes varias tareas asíncronas independientes y quieres lanzarlas al mismo tiempo, Promise.all es la herramienta ideal:

index.js
Output
Click Run to see the output here.

Los tres temporizadores corren en paralelo. Promise.all se resuelve con un array de resultados en el mismo orden que la entrada, en cuanto todas las promesas se cumplen. El tiempo total ronda los 400ms, no los 900ms.

Ojo con el detalle: Promise.all rechaza apenas alguna de sus promesas falla, y el resto de resultados se pierden. Ese comportamiento es justo lo que quieres cuando necesitas todas las piezas sí o sí (por ejemplo, al renderizar una página que depende de tres llamadas a una API). Si no es tu caso, mejor tira de allSettled.

Cuando algunos fallos están permitidos: Promise.allSettled

Promise.allSettled espera a que cada promesa termine (se cumpla o se rechace) y te devuelve un informe con el resultado de cada una:

index.js
Output
Click Run to see the output here.

Cada resultado es un objeto: { status: "fulfilled", value } o { status: "rejected", reason }. Resulta muy útil cuando puedes permitirte un éxito parcial: registrar un lote de eventos, descargar un montón de miniaturas o lanzar health checks independientes.

Hay otros dos combinadores que conviene tener en el radar:

  • Promise.race([...]): se resuelve en cuanto la primera promesa se asienta, sea cumplida o rechazada. Muy práctico para implementar timeouts.
  • Promise.any([...]): se cumple con el primer éxito e ignora los rechazos. Solo falla si todas las promesas son rechazadas.

Las promesas siempre son asíncronas

Incluso una promesa que ya está resuelta ejecuta su callback de .then de forma asíncrona, nunca síncronamente ni en el mismo tick:

index.js
Output
Click Run to see the output here.

La salida es antes, después, inmediato. El callback de .then espera a que termine el código actual y luego se ejecuta en la cola de microtareas. Esta regla —"el callback de una promesa nunca se ejecuta de forma síncrona"— es la razón por la que combinar promesas con código síncrono resulta predecible: el código síncrono siempre termina primero.

Lo que viene: async/await

Encadenar llamadas a .then funciona, pero cuando tienes más de dos o tres pasos la cosa empieza a parecerse a una escalera. async/await es azúcar sintáctico sobre las promesas que te permite escribir la misma lógica como si fuera síncrona: con try/catch para los errores y variables normales para los valores intermedios. Eso lo vemos a continuación.

Preguntas frecuentes

¿Qué es una Promise en JavaScript?

Una Promise es un objeto que representa un valor que todavía no está listo, normalmente el resultado futuro de una operación asíncrona como una petición de red. Siempre se encuentra en uno de estos tres estados: pending, fulfilled o rejected. Para leer el valor final se enganchan callbacks con .then() y .catch().

¿Cuál es la diferencia entre then y catch?

.then(onFulfilled) se ejecuta cuando la Promise se resuelve correctamente y recibe el valor resuelto. .catch(onRejected) se dispara cuando la Promise (o cualquier otra anterior en la cadena) falla y recibe el error. Un único .catch() al final de la cadena basta para capturar los errores de todos los pasos anteriores.

¿Qué hace Promise.all?

Promise.all([p1, p2, p3]) recibe un array de promesas y devuelve una sola promesa que se resuelve con un array con todos los valores, pero solo cuando todas las promesas de entrada se han cumplido. Si una sola falla, el conjunto se rechaza de inmediato. Si quieres obtener el resultado de todas aunque alguna falle, tira de Promise.allSettled.

¿Mejor promesas o async/await?

Por debajo es la misma maquinaria: async/await no es más que azúcar sintáctico sobre las promesas. El código nuevo suele leerse mejor con async/await, pero sigues devolviendo promesas, sigues capturando errores con try/catch o .catch() y sigues recurriendo a Promise.all cuando quieres lanzar cosas en paralelo. Entender bien las promesas es lo que hace que async/await cobre sentido.

Aprende a programar con Coddy

COMENZAR