WTF ¿Qué es una promesa en Javascript?
Matías Hernández Arellano
Posted on September 7, 2022
¿WTF son las promesas?
Promises es es un objeto para manipular acciones asíncronas dentro de Javascript.
Javascript es un lenguaje que se ejecuta en el browser con la idea de dar dinamismo al contenido ahí desplegado, esto implica que muchas operaciones se deben dar de forma asíncrona. Dado que los motores de javascript son "Single Threaded", es decir, Javascript no maneja múltiples hilos es necesario implementar otra estrategia para la asincronía.
Una Promise es un objeto que produce un valor que estará disponible en algún momento futuro.
El concepto de promesa, future o deferred es una construcción utilizada para “sincronizar” la ejecución de instrucciones en lenguajes concurrentes. Este concepto describe un objeto cuyo valor inicial es desconocido.
El desarrollo web trajo nueva vida al concepto dada su naturaleza asíncrona y el modelo de request-response
utilizado. (solicitar y obtener datos mediante una red donde el solicitante queda a la "espera" de una respuesta).
Si te interesa saber más del concepto de promises y futures puedes encontrar el paper original aquí o en esta sección sobre concurrencia del libro “Clojure for the Brave and True”
Las Promises como hoy las conocemos no llegaron a Javascript hasta la versión ES6, antes de eso todo era un caos y cada quien implementaba comportamientos asíncronos como mejor le pareciera 🤯
Eras muy joven pero antes teniamos múltiples librerías para crear promesas como por ejemplo bluebird o q
¿Cómo funcionan las promesas?
Un promesa es un objeto, y como tal tiene varios atributos y métodos que nos permiten trabajar con el. La idea de este objeto es poder acceder y trabajar con el de forma sincrona
pero cuyo valor es asincrono
. Un gato de Schrödinger.
Una promesa tiene 3 estados.
- Pending: Es el valor inicial, indicando que está a la espera de que “el trabajo” sea realizado.
- Fullfilled: Cuando la operación se ejecuta de forma existosa.
- Rejected: Cuando la operación falla de alguna manera.
Ahora bien, estos estados no son expuestos como tal, una promesa es más bien “una caja negra” en donde sólo la función que creo la promesa tiene conocimiento de su propio estado.
¿Cómo se crea una promesa?
Suficiente texto, algo de código
const sleep = time => new Promise((resolve, reject) => setTimeout(resolve, time));
sleep(3000)
console.log('¿Funcionó la promesa?')
¿Qué ocurre al ejecutar este código?
Nada fuera de lo esperado 😅.
En este código creamos una función sleep
que recibe el parámetro time
, un número y retorna una promesa.
Para crear una promesa usamos su constructor y la palabra reserva new
. La promesa recibe una función tipo callback
como único argumento. Esta función recibe dos parámetros. resolve
y reject
.
Estas funciones resolve
y reject
son funciones pre-definidas por el motor de Javascript para manejar los estados de la promesa, nosotros sólo debemos utilizarlas cuando corresponde.
-
resolve
Es utilizado para marcar que el proceso de la promesa terminó de forma exitosa.resolve
acepta un parámetro que es retornado como resultado de la ejecución. -
reject
Se usa para determinar cuando el proceso falló, recibe un argumento que indica la razón del rechazo de la promesa.
Entonces, en el código, nuestra función callback es:
(resolve, reject) => setTimeout(resolve, time)
Donde, time
es el argumento de la función externa (👋 Mira! una closure salvaje a aparecido). setTimeout
ejecutará la función que se usa como argumento cuando el tiempo definido pase, es decir cuando time
se cumpla. La función que pasamos como argumento es resolve
, entonces setTimeout
ejecutará resolve
cuando el tiempo indicado termine.
Si quieres saber más sobre el concepto de closures te comparto dos artículos que escribí al respecto.
Uno que puedes encontrar en FreeCodeCamp (en español) y otro similar en en mi sitio web
Esta promesa en particular no falla jamas, por lo que podemos reescribirla así
const sleep = time => new Promise((resolve) => setTimeout(resolve, time));
Simplemente, eliminando el argumento reject
.
Solo puedes utilizar un sólo resultado o error dentro de una promesa.
La función "ejecutora" sólo puede tener una llamada y solo una a
resolve
o una areject
. Estas funciones cambian el estado de la promesa y este cambio de estado es único e irreversible.Se recomienda rechazar con errores
El método
reject
acepta cualquier valor (al igual queresolve
) pero dado el contexto de "rechazo”, se recomienda usar objetosError
para indicar la razón del rechazo.
¿Por qué el console.log
no se ejecuta después de 3 segundos?
Esto es por que nuestro código no indica que se espere el resultado de la promesa.
Podemos pensar en la promesa como un enlace entre a función asíncrona encerrada en la caja negra y el código que necesita consumir su resultado. Para esto, Promise
permite registrar o "suscribirse" a la espera del resultado usando los métodos .then
, .catch
, .finally
. Una Promise
es un objeto theneable
.
.then
El método .then
es el más importante y fundamental, es el que nos permite "leer” el contenido de la caja negra, su sintaxis es
promise.then(
function(result) {},
function(error) {}
);
Es decir, recibe dos argumentos tipo función para manejar cada uno de los posibles estados de la promesa: Fullfilled
y Rejected
.
El primer argumento es una función que se ejecutará cuando la promesa es resuelta existosamente, es decir, cuando se usa resolve
. Esta función recibe como argumento el valor pasado al método resolve
.
El segundo argumento, es otra función pero que esta vez se ejecuta cuando la promesa es rechazada, y recibe como parámetro la razón de ese rechazo: el valor pasado a reject
.
Entonces, ahora podemos modificar nuestro ejemplo utilizando .then
const sleep = time => new Promise((resolve, reject) => setTimeout(resolve, time));
sleep(3000).then(() => {
console.log('¿Funcionó la promesa?')
})
Ahora, concatenamos la ejecución de console.log
a la espera de que la promesa sleep
se cumpla y en ese momento se muestra el mensaje. Y como en nuestro caso resolve
no recibe ningún valor, nuestro función de "èxito" tampoco recibe parámetros y solo se ejecuta.
Es posible separar el manejo de errores y de ejecuciones exitosas para dar un poco de legibilidad al código, para eso podemos sólo usar el primer argumento de .then
y luego usar el siguiente método: .catch
.catch
Este método sólo se ejecuta cuando la promesa fue rechazada usando reject
. Este método es un análogo de usar .then(null, f)
(siendo f
una función), existe solo por claridad de su ejecución.
¿Pero que ocurre si quieres ejecutar algún proceso sin importar si la promesa se cumplió o no?. Entra .finally
.finally
Muy similar a como funciona .finally
en un bloque try/catch
este ejecutará siempre la función que se le pase como argumento, independiente del resultado de la promesa.
- La función utilizada dentro de
finally
no recibe argumentos. No sabemos si la promesa fue exitosa o no. - Puedes concatenar otros “manejadores” después de
finally
.
const sleep = time => new Promise((resolve, reject) => setTimeout(resolve, time));
sleep(3000)
.finally(() => console.log('Promesa está lista'))
.then(() => {
console.log('¿Funcionó la promesa?')
})
.catch(err => {
console.log(erro)
})
Algunas “reglas”
- Una promesa es
thenable
, el objeto siempre otorga un métodothen
- Una promesa pendiente puede transicionar al estado
fullfilled
orrejected
- Una promesa en estado
fullfilled
orejected
se conoce comosettled
y no puede cambiar de estado nuevamente.
Encadenamiento
Dado que .then
siempre retorna una nueva promesa, es posible encadenar promesas otorgando mas control sobre como se manejan los resultados y errores, un ejemplo muy utilizado en el mundo real es trabajando con la api fetch
.
La api fetch
es la interfaz nativa de javascript para obtener recursos externos y funciona como una Promesa.
const obtenerDatos = () => {
return fetch('/algunaUrl/)
}
obtenerDatos()
.then(response => response.json())
.then(json => json)
.then(print => {
console.log(print)
})
.catch(error => console.error(error)
En este ejemplo creamos una función que utiliza la api fetch
para obtener datos de algún endpoint y retorna una promesa. Podemos entonces usar .then
para leer la respuesta obtenida.
La respuesta de una llamada a fetch
es un objeto Response
que tiene un método que permite obtener el cuerpo de la respuesta en forma json y que es en si mismo una promesa.
.then(response => response.json())
Usamos el retorno implícito de la arrow function para retornar inmediatamente la promesa. Luego volvemos a encadenar usando then
y obtenemos el valor retornado previamente
. then(json => json)
En este punto, podemos ya utilizar el valor, pero solo por propositos de demostración, lo retornamos json => json
y volvemos a encadenar .then(print => {})
creando una nueva función que se encarga de imprimir el resultado.
Finalmente encadeamos un .catch
para capturar cualquier error.
Algunos otros métodos.
El objeto Promise
nos ofrece algunos otros métodos "utilitarios”:
-
Promise.all
permite obtener el resultado de un grupo de promesas. Recibe como argumento uniterable
como un arreglo de promesas y espera a que todas las promesas se cumplan o a que una sea rechazada para retornar. Si todas las promesas se cumplen, retornará al siguiente.then
un arreglo con los resultados en el mismo orden que las promesas pasadas como argumento. -
Promise.race
También recibe un arreglo de promesas pero retorna tan pronto una promesa se cumple o rechaza. -
Promise.allSettled
También recibe uniterable
de promesas y espera a que todas las promesas hayan terminado sin importar si fueron exitosas o rechazadas. Retorna una promesa con un arreglo de objetos con los resultados de cada promesa pasada como argumento. -
Promise.any
. Recibe uniterable
y retorna tan pronto una de las promesas termina exitosamente.
Desafío
Escribe una promesa que te permita cargar un script de forma asíncrona y te comunique cuando se ejecuto exitosamente o falló. Puedes usar el código base disponible aquí.
✉️ Únete a Micro-bytes 🐦 Sígueme en Twitter ❤️ Apoya mi trabajo
Posted on September 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.