Promises (Promesas) - Básico
Clase 2 de 512 min de lectura
¿Recuerdas la pirámide de callbacks de la clase anterior, cada vez más escorada a la derecha? Las Promises (promesas) nacieron para acabar con ella: el mismo código asíncrono, pero que se lee de arriba abajo.
Una Promise es un objeto que representa el resultado futuro de una operación asíncrona: o termina bien y te da un valor, o falla y te da un error.
¿Qué es una Promise?#
Imagina que pides una pizza por teléfono y la pizzería te da un número de pedido. Ese número no es la pizza, pero es la promesa de que acabará llegando.
Con ese número en la mano solo pueden pasar tres cosas:
- Sigues esperando a que llegue la pizza (pending).
- La recibes cuando el repartidor llama al timbre (fulfilled).
- Te avisan de que no pueden entregarla (rejected).
En JavaScript una Promise funciona igual: es un objeto que representa un valor que todavía no tienes, pero que tendrás (o no) más adelante.
¿Por qué son importantes las Promises?
Porque resuelven tres dolores de cabeza de los callbacks:
- Adiós al Callback Hell. Ya no hay que anidar una función dentro de otra sin fin.
- Errores en un solo sitio. Un único
.catch()captura los errores de toda la cadena. - Operaciones que se encadenan. Cada paso va debajo del anterior y se lee de un tirón.
Los tres estados de una Promise
Una Promise está siempre en uno de estos tres estados:
| Estado | Significado |
|---|---|
| ⏳ Pending (pendiente) | Estado inicial: la operación aún no ha terminado |
| ✅ Fulfilled (cumplida) | La operación terminó bien y hay un valor |
| ❌ Rejected (rechazada) | La operación falló y hay un error |
Diagrama: El ciclo de vida de una Promise
reject() posterior se ignora.Cuando una Promise deja de estar pendiente, ya no cambia nunca más. Si se cumplió con un valor, se queda cumplida con ese valor para siempre. La pizza entregada no se «desentrega».
Creando tu primera promesa#
Para entender cómo funciona por dentro, vamos a crear una desde cero con el constructor new Promise(). Le pasamos una función, llamada ejecutor, que recibe dos parámetros: resolve y reject. Con ellos indicamos si la promesa se cumple o se rechaza.
En el ejemplo simulamos con setTimeout una operación que tarda 2 segundos. Mira qué se llama en cada rama del if:
const miPromesa = new Promise((resolve, reject) => { // Simulamos una operación que toma tiempo console.log('🔄 La Promise está pendiente...') setTimeout(() => { const exito = true // Cambia esto a false para ver el rechazo if (exito) { console.log('✅ Resolviendo la Promise...') resolve('¡Operación exitosa!') // Cumplimos la promesa } else { console.log('❌ Rechazando la Promise...') reject('Algo salió mal') // Rechazamos la promesa } }, 2000)})console.log('Promise creada:', miPromesa)// Consumir la PromisemiPromesa .then(resultado => console.log('Resultado:', resultado)) .catch(error => console.log('Error:', error))Al crearla, la promesa queda pendiente. Dos segundos después llamamos a resolve y el valor '¡Operación exitosa!' le llega al .then(). Si cambias exito a false, será el .catch() quien reciba el error.
Anatomía de una Promise
Quitando el ejemplo de en medio, el esqueleto de cualquier promesa es este:
const promesa = new Promise((resolve, reject) => { // resolve: función para cumplir la promesa con un valor // reject: función para rechazar la promesa con un error // Tu código asíncrono aquí...})Pieza a pieza:
new Promise()crea un nuevo objeto Promise.- El ejecutor (la función que le pasas) se ejecuta inmediatamente, no espera a nada.
resolveyrejectson funciones que te da JavaScript; tú solo las llamas.resolve(valor)cambia el estado a fulfilled con ese valor.reject(error)cambia el estado a rejected con ese error.
const promesa = new Promise((resolve, reject) => { resolve('¡Operación exitosa!') reject('Algo salió mal') // ⚠️ Ignorado: ya se llamó a resolve})Consumiendo Promises: .then() y .catch()#
Tener una promesa no sirve de mucho si no recoges su resultado. A eso se le llama consumirla, y se hace con dos métodos:
.then()se ejecuta cuando la promesa se cumple, y recibe el valor..catch()se ejecuta cuando la promesa se rechaza, y recibe el error.
¿Cómo funcionan .then() y .catch()?
Piensa en ellos como suscripciones a algo que pasará en el futuro. Le dices a JavaScript: «cuando esta promesa termine, ejecuta esta función».
La función crearPromesa devuelve una promesa que se cumple o se rechaza según el parámetro exito. Así podemos probar los dos caminos:
function crearPromesa(exito) { return new Promise((resolve, reject) => { console.log('⏳ Procesando...') setTimeout(() => { if (exito) { resolve('¡Todo salió bien! 🎉') } else { reject('UNA CALAMIDAD 💥') } }, 1000) })}// Consumiendo la promesa exitosacrearPromesa(true) .then((resultado) => { console.log('✅ Éxito:', resultado) return resultado.toUpperCase() // Puedes transformar el valor }) .then((resultadoMayusculas) => { console.log('📝 Transformado:', resultadoMayusculas) // -> 📝 Transformado: ¡TODO SALIÓ BIEN! 🎉 }) .catch((error) => { // Esto nunca se ejecutará porque la promesa se cumple console.log('❌ Error:', error) })// Probando con error (falla al cabo de 1 segundo)crearPromesa(false) .then((resultado) => { // Este then se salta: la promesa se ha rechazado console.log('✅ Éxito:', resultado) }) .catch((error) => { console.log('❌ Error capturado:', error) // -> ❌ Error capturado: UNA CALAMIDAD 💥 })Dos detalles que conviene no pasar por alto. En el primer caso, lo que devuelves en un .then() es lo que recibe el siguiente. En el segundo, al rechazarse la promesa se saltan todos los .then() y la ejecución va directa al .catch().
Encadenamiento de Promises#
Ese truco de poner un .then() detrás de otro es uno de los superpoderes de las promesas. Funciona porque cada .then() devuelve una nueva Promise, y sobre ella puedes volver a llamar a .then().
¿Cómo funciona el encadenamiento?
Imagina la línea de producción de una fábrica de papas fritas (o patatas, según donde vivas):
- Entra una papa cruda.
- Se pela y se corta.
- Se fríe.
- Se le añade sal.
- Se empaqueta.
Cada paso transforma el resultado del anterior. Con promesas se escribe casi igual que se cuenta:
// Encadenamiento: línea de producción de papas fritasagarrarPapaCruda() // devuelve una promesa con la papa cruda .then(papaCruda => pelarYCortar(papaCruda)) // pela y corta la papa .then(papaCortada => freir(papaCortada)) // fríe las papas cortadas .then(papasFritas => añadirSal(papasFritas)) // añade sal a las papas fritas .then(papasConSal => empaquetar(papasConSal)) // empaqueta el producto final .then(productoFinal => { console.log('🎉 ¡Papas fritas listas!:', productoFinal) // -> 🎉 ¡Papas fritas listas!: "📦 Papas fritas empaquetadas" }) .catch(error => { console.log('❌ Error en la línea de producción:', error) // Si cualquier paso falla, se ejecuta este catch })agarrarPapaCruda() devuelve una promesa con la papa cruda. El primer .then() la recibe y devuelve otra promesa con la papa pelada y cortada; el siguiente recibe esa y devuelve las papas fritas, y así hasta el final.
¿Y si la freidora se estropea a mitad de camino? Los pasos que quedan se saltan y el error cae en el .catch(), que vigila cualquier punto de la cadena.
- 1🥔promesa inicialagarrarPapaCruda()→ papa cruda
- 2🔪.then()pelarYCortar→ papa cortada
- 3🍟.then()freir→ papas fritas✕ falla al freír
- 4🧂.then()añadirSal→ con salsaltado
- 5📦.then()empaquetar→ empaquetadassaltado
- 6🎉.then()final => …→ ¡listas!saltado
.then() que faltan y se ejecuta el único .catch().Espera... ¡todavía mejor!
Fíjate en que cada flecha solo recibe un valor y se lo pasa tal cual a otra función. Como en JavaScript las funciones se pueden pasar como parámetros, puedes darle la función directamente al .then() y ahorrarte el intermediario.
Funciona porque todas cumplen el mismo contrato: reciben un único parámetro y devuelven una promesa. Así queda mucho más limpio:
agarrarPapaCruda() // devuelve una promesa con la papa cruda .then(pelarYCortar) // pela y corta la papa .then(freir) // fríe las papas cortadas .then(añadirSal) // añade sal a las papas fritas .then(empaquetar) // empaqueta el producto final .then(productoFinal => { console.log('🎉 ¡Papas fritas listas!:', productoFinal) // -> 🎉 ¡Papas fritas listas!: "📦 Papas fritas empaquetadas" }) .catch(error => { console.log('❌ Error en la línea de producción:', error) // Si cualquier paso falla, se ejecuta este catch })Ventajas sobre Callbacks#
Hora de comparar con un caso realista: buscar un usuario, sus pedidos, el detalle del primer pedido y calcular el total. Primero con callbacks, comprobando el error en cada paso:
// El temido "Callback Hell" o "Pirámide de la Perdición"obtenerUsuario(1, (error, usuario) => { if (error) { console.log('Error:', error) } else { obtenerPedidos(usuario.id, (error, pedidos) => { if (error) { console.log('Error:', error) } else { obtenerDetalles(pedidos[0].id, (error, detalles) => { if (error) { console.log('Error:', error) } else { calcularTotal(detalles, (error, total) => { if (error) { console.log('Error:', error) } else { console.log('Total:', total) // Y esto puede seguir y seguir... } }) } }) } }) }})Lo que hace daño a la vista también hace daño al mantenerlo:
- Forma de pirámide: hay que leer en diagonal.
- Manejo de errores repetido en cada nivel, cuatro veces el mismo
if. - Difícil de depurar y de reutilizar: cada trozo vive dentro del anterior.
Con Promises (más elegante y limpio)
Ahora la misma funcionalidad con promesas. La clave está en cada return: devolvemos la siguiente promesa para que la cadena continúe.
// Misma funcionalidad, pero con PromisesobtenerUsuario(1) .then(usuario => { console.log('Usuario:', usuario.nombre) return obtenerPedidos(usuario.id) // Devolvemos la siguiente Promise }) .then(pedidos => { console.log('Pedidos:', pedidos) return obtenerDetalles(pedidos[0].id) // Devolvemos la siguiente Promise }) .then(detalles => { console.log('Detalles:', detalles) return calcularTotal(detalles) // Devolvemos la siguiente Promise }) .then(total => { console.log('Total:', total) // ¡Terminamos! Sin pirámides, sin anidamiento }) .catch(error => { // Un solo .catch() maneja TODOS los errores de toda la cadena console.log('❌ Error en cualquier paso:', error) })La diferencia salta a la vista:
- Código lineal: se lee de arriba abajo.
- Un solo
.catch()para todos los errores. - Pasos separados: cada uno se puede depurar y reutilizar por su cuenta.
- Fácil de ampliar: añadir un paso es añadir un
.then().
Resumiendo lo aprendido sobre Promesas#
Ya sabes qué es una promesa, cómo se crea y cómo se consume. Te dejo las tres piezas juntas, para cuando necesites refrescarlas:
| Para... | Usas |
|---|---|
| Crear una promesa | new Promise((resolve, reject) => { ... }) |
| Cumplirla o rechazarla | resolve(valor) o reject(error) |
| Recoger el valor | .then() |
| Capturar el error | .catch() |
Próximo paso
En la siguiente clase subimos el nivel: Promise.all(), Promise.race(), un manejo de errores más fino y patrones que usarás en aplicaciones reales.
En resumen
- Una Promise es un objeto que representa un valor futuro: el resultado de una operación asíncrona.
- Tiene tres estados (pending, fulfilled y rejected) y, cuando sale de pending, ya no cambia.
- Se crea con
new Promise((resolve, reject) => ...)y se consume con.then()y.catch(). - Cada
.then()devuelve una nueva promesa, por eso se pueden encadenar. - Un solo
.catch()al final captura los errores de toda la cadena.