Antes que te vayas
Inscríbete en nuestro curso gratuito de Laravel
Las Promises (en adelante "Promesas") en Javascript, marcan un antes y un después en la historia del desarrollo web.
En este momento, te debes encontrar en alguna de las siguientes situaciones:
Has leído "Promises" en varios lugares. Sabes que es un concepto importante en el ecosistema de Javascript, pero no estás seguro de lo que es una Promesa. Si esta es tu situación, lo recomendable es que leas todo el artículo.
Has usado las Promesas de Javascript en alguna ocasión anteriormente. Sin embargo, nunca terminaste de entender la importancia y utilidad de este concepto en su totalidad. Si ese es tu caso, puedes empezar revisando la terminología.
JavaScript es "single threaded". Eso significa que sólo puede realizar una acción a la vez, desde el único hilo de ejecutación disponible.
Si tenemos una secuencia de operaciones, éstas operaciones se deben ejecutar una después de otra (ya que no es posible crear más hilos).
La implementación de JavaScript es distinta en cada navegador. Pero generalmente la ejecución de código JavaScript ocurre a la par con el proceso de pintar elementos, actualizar los estilos, y gestionar acciones del usuario (como resaltar texto o interactuar con los controles de un formulario). La actividad en una de estas cosas retrasa a las otras.
Por ejemplo:
Como ser humano, eres multihilo. Puedes escribir con varios dedos. Puedes caminar y mantener una conversación al mismo tiempo.
Sin embargo hay operaciones de bloqueo con las que tenemos que lidiar. Por ejemplo, al estornudar.
Otras actividades son forzadas a suspenderse durante el estornudo.
Eso es bastante molesto, especialmente cuando estás muy concentrando haciendo múltiples actividades en simultáneo.
En JS, la solución a esta limitante son los events
y callbacks
. Probablemente los has usado.
Aquí un ejemplo de eventos:
const myImage = document.querySelector('#example');
myImage.addEventListener('load', function() {
// bien! la imagen cargó correctamente
});
myImage.addEventListener('error', function() {
// sucedió algún inconveniente
});
Todo bien hasta ahora.
Obtenemos una referencia de la imagen, agregamos un par de listeners
, y nuestro código JavaScript se volverá a ejecutar cuando uno de estos eventos ocurra.
Sin embargo, en el ejemplo anterior, es posible que los eventos hayan ocurrido antes de empezar a escucharlos. Por eso es importante verificar ello (en este caso evaluando la propiedad complete
):
var myImage = document.querySelector('#example');
function loaded() {
// bien, la imagen cargó
}
if (myImage.complete) {
loaded();
} else {
myImage.addEventListener('load', loaded);
}
myImage.addEventListener('error', function() {
// ocurrió un imprevisto
});
Aquí no capturamos el error en caso que haya ocurrido antes de registrar el evento (desafortunadamente el DOM tampoco nos permite hacer ello).
Pero lo importante aquí es, las consideraciones que debemos tener en cuenta para cargar una imagen.
¿Cómo sería el código si nos interesa ejecutar acciones luego que un conjunto de imágenes han cargado?
Los eventos vienen muy bien para detectar acciones que se repiten múltiples veces sobre un mismo objeto (como keyup
, touchstart
, etc).
En tales casos, no es relevante lo que ha ocurrido antes. Nos interesan las acciones que se detectan una vez que se han empezado a escuchar los eventos.
Pero, cuando hay que lidiar con acciones asíncronas, que pueden tener éxito (success
) o bien fallar (failure
), idealmente querríamos algo como lo siguiente:
myImage.ejecutarEstoSiYaCargoOCuandoCargue(function() {
// la imagen cargó
}).oSiFallaEjecutarEstoOtro(function() {
// falló la carga
});
// y así mismo …
cuandoTodasHayanCargado([myImage1, myImage2]).ejecutarEsto(function() {
// todas las imágenes han cargado
}).oSiAlgoFalloEjecutarEsto(function() {
// falló la carga de una imagen, en algún punto
});
Pues, esto es exactamente lo que hacen las Promesas (hacen esto posible, pero con mejores nombres).
Si los elementos HTML img
tuviesen un método "ready" que devolviese una Promesa, podríamos hacer esto:
myImage.ready().then(function() {
// cargó
}, function() {
// falló
});
// y así mismo …
Promise.all([myImage1.ready(), myImage2.ready()]).then(function() {
// cargaron todas
}, function() {
// ocurrió un fallo
});
Básicamente, las Promesas son similares a los Eventos, con las siguientes diferencias:
callback
de success
o failure
, la función de callback
correspondiente será llamada (incluso si el evento tuvo lugar antes).Esto resulta muy útil para operaciones asíncronas, porque más allá de capturar el momento exacto en que ocurre algo, nos enfocamos en reaccionar ante lo ocurrido.
Tenemos muchos términos relacionados a lo que son Promesas en Javascript. A continuación veamos lo más básico.
Una promesa puede presentar los siguientes estados:
fulfilled
- La acción relacionada a la promesa se llevó a cabo con éxitorejected
- La acción relacionada a la promesa fallópending
- Aún no se ha determinado si la promesa fue fulfilled
o rejected
settled
- Ya se ha determinado si la promesa fue fulfilled
o rejected
También se suele usar el término thenable
, para indicar que un objeto tiene disponible un método "then" (y que por tanto está relacionado con Promesas).
Muchas veces necesitamos ejecutar 2 o más operaciones asíncronas una tras otra. Es decir, la siguiente operación empieza luego que la anterior se ejecutó con éxito (ya sea porque la acción anterior era necesaria para preparar algo, o porque devuelve un resultado que se usará en las siguientes).
Esto se resuelve fácilmente usando una secuencia de Promesas en cadena.
Pero veamos primero, cómo se lidiaba anteriormente con operaciones asíncronas continuas. Esto nos llevaba a lo que se conoce como callback hell
.
Supongamos que necesitamos ejecutar 3 acciones. Nuestro código usando callbacks
se vería de esta manera:
hacerAlgo(function(resultado) {
hacerAlgoMas(resultado, function(nuevoResultado) {
hacerUnaTerceraCosa(nuevoResultado, function(resultadoFinal) {
console.log('Resultado final: ' + resultadoFinal);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Esto se ve muy mal. Pero es lo que se ha usado tradicionalmente.
Si ya tienes tiempo usando Javascript, de seguro que lo entiendes muy bien.
Pero en caso que no, esto es lo que ocurre:
callback
. Esta función se llamará posteriormente (tras hacer algo), y recibirá un parámetro "resultado".callback
(que espera un "nuevoResultado").callback
que define "hacerUnaTerceraCosa" es la última en llamarse (si sale todo bien), e imprime por consola el resultado final.failureCallback
. Es decir, cada función que invocamos tiene sus correspondientes callbacks
de éxito y fallo. La diferencia es que los callbacks
de éxito aparecen definidos en el ejemplo, mientras que los de fallo se asume que están definidos (pero sólo indicamos su nombre).Veamos un ejemplo más específico.
Tenemos una aplicación web que consulta una API para obtener información. Cada llamado a la API implica un tiempo de espera.
Para evitar que esto afecte a toda nuestra aplicación es importante que estas operaciones ocurran de forma asíncrona.
Queremos mostrar la lista de artículos más populares, de las categorías preferidas por el usuario que ha iniciado sesión (o los artículos más vistos en general si el usuario no ha seleccionado categorías).
Pero, primero queremos saber si el usuario ha confirmado su correo, porque sino, primero le vamos a sugerir hacer ello.
Entonces el código que hemos de usar sería similar al siguiente:
verificarSiYaConfirmoSuCorreo(function(correoConfirmado) {
if (correoConfirmado) {
obtenerCategoriasPreferidas(userId, function(categoriasPreferidas) {
if (categoriasPreferidas.length > 0)
obtenerArticulosPopularesEn(categoriasPreferidas, function(listaArticulos) {
console.log('Artículos de las categorías preferidas: ' + listaArticulos);
}, failureCallback);
else
obtenerArticulosPopulares(function(listaArticulos) {
console.log('Artículos más vistos en general: ' + listaArticulos);
}, failureCallback);
}, failureCallback);
} else {
console.log('Primero por favor confirma tu correo');
}
}, failureCallback);
Como debes imaginar, la situación se torna más complicada a medida que necesitamos hacer más operaciones asíncronas, una otras otra.
Para evitar que esto crezca en profundidad podemos hacer uso de Promesas.
El equivalente usando Promesas, podría escribirse de esta manera:
verificarSiYaConfirmoSuCorreo()
.then(function(correoConfirmado) {
if (correoConfirmado)
return obtenerCategoriasPreferidas(userId);
else
throw new Error('Primero por favor confirma tu correo');
})
.then(function(categoriasPreferidas) {
if (categoriasPreferidas.length > 0)
return obtenerArticulosPopularesEn(categoriasPreferidas);
else
return obtenerArticulosPopulares();
})
.then(function(listaArticulos) {
console.log('Artículos a mostrar: ' + listaArticulos);
})
.catch(failureCallback);
Es importante tener en cuenta que estos últimos ejemplos pueden escribirse de la siguienta manera (desde ES6, usando arrow functions
):
verificarSiYaConfirmoSuCorreo()
.then(correoConfirmado => {
if (correoConfirmado)
return obtenerCategoriasPreferidas(userId);
else
throw new Error('Primero por favor confirma tu correo');
})
.then(categoriasPreferidas => {
if (categoriasPreferidas.length > 0)
return obtenerArticulosPopularesEn(categoriasPreferidas);
else
return obtenerArticulosPopulares();
})
.then(listaArticulos => console.log('Artículos a mostrar: ' + listaArticulos))
.catch(failureCallback);
Las Promesas en JS son justamente ello. Prometen que algo está por resolverse, y nosotros, conoceremos si eso se llevó a cabo con éxito o no, en breve.
Más técnicamente hablando:
Un objeto Promise
representa la finalización (o falla) eventual de una operación asíncrona y su valor resultante.
En los ejemplos anteriores hemos visto cómo podemos usar las promesas. Pero no hemos visto cómo declarar nuestros propios objetos Promise
.
Por cada then
en los ejemplos previos, debemos asumir que la expresión que está antes es o devuelve una promesa.
Un objeto Promise
representa un valor, que no se conoce necesariamente al momento de crear la promesa.
Esta representación nos permite realizar acciones, con base en el valor de éxito devuelto, o la razón de fallo.
Es decir, los métodos asíncronos producen valores que aún no están disponibles. Pero la idea ahora es que, en vez de esperar y devolver el valor final, tales métodos devuelvan un objeto Promise
(que nos proveerá del valor resultante en el futuro).
Hoy en día, muchas bibliotecas JS se están actualizando para hacer uso de Promesas, en vez de simples funciones callback
.
Nosotros también podemos crear nuestras promesas, basados en esta sintaxis:
new Promise(function(resolve, reject) { ... });
Este constructor es usado principalmente para envolver funciones que no soportan el uso de Promesas.
El constructor espera una función como parámetro. A esta función se le conoce como executor
.
Esta función executor
recibirá 2 argumentos: resolve
y reject
.
La función executor
es ejecutada inmediatamente al implementar el objeto Promise
, recibiendo las funciones resolve
y reject
para su uso correspondiente. Esta función executor
es llamada incluso antes que el constructor Promise
devuelva el objeto creado.
Las funciones resolve
y reject
, al ser llamadas, "resuelven" o "rechazan" la promesa. Es decir, modifican el estado de la promesa (como hemos visto antes, inicialmente es pending
, pero posteriormente puede ser fulfilled
o rejected
).
Normalmente el executor
inicia alguna operación asíncrona, y una vez que ésta se completa, llama a la función resolve
para resolver la promesa o bien reject
si ocurrió algo inesperado.
Si la función executor
lanza algún error, la promesa también es rejected
.
executor
es ignorado.Veamos un ejemplo.
"Te prometo que en 3 segundos te querré", se puede traducir a código de la siguiente manera:
var myPromise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Te quiero');
}, 3000);
});
Hasta este punto, a pesar que no hemos usado then
sobre la promesa, para actuar según su valor resultante, la función executor
ya se ha ejecutado.
Si estás usando la consola interactiva del navegador (DevTools en el caso de Chrome) y me sigues, ahora puedes ejecutar:
myPromise.then(function(value) {
console.log(value);
});
Y verás que el resultado (con que se resuelve la promesa) está disponible inmediatamente (sin esperar los 3 segundos). Esto demuestra lo que hablábamos antes.
Pero, si en cambio, ejecutamos todo esto:
var myPromise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hola');
}, 3000);
});
myPromise.then(function(value) {
console.log(value);
});
console.log(myPromise);
O lo que es lo mismo:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Hola'), 3000);
});
myPromise.then(value => console.log(value));
console.log(myPromise);
Lo que ha de ocurrir es lo siguiente:
executor
y se crea nuestro objeto Promise
.then
, expresando qué es lo que queremos hacer con el valor que devolverá la promesa.[object Promise]
.executor
, se resuelve la promesa, y terminamos mostrando por consola el mensaje "Hola".Veamos un último ejemplo.
Supongamos que queremos contar hasta 3. Queremos imprimir un número por consola tras cada segundo.
Una forma sería:
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
}, 1000);
}, 1000);
}, 1000);
Tal vez se te ha ocurrido 3 setTimeout
independientes, para 1000, 2000 y 3000 milisegundos. Pero digamos que eso no está permitido en este caso.
¿Qué tal si definimos una función que ejecute otra función en un segundo?
function unSegDespues(otraFunc) {
setTimeout(otraFunc, 1000);
}
Entonces tendríamos:
unSegDespues(() => {
console.log(1);
unSegDespues(() => {
console.log(2);
unSegDespues(() => console.log(3));
});
});
Simplifica un poco, porque no repetimos el argumento de los 1000 milisegundos. Pero el callback hell
sigue presente.
¿Qué tal si devolvemos una promesa que se resuelve en 1 segundo?
function esperar1Seg() {
return new Promise(function (resolve, reject) {
setTimeout(resolve, 1000);
});
}
Nótese que devolvemos la instancicación de una promesa. No la instanciamos directamente (eso causaría que el executor
se inicie en este momento, y ello no nos resultaría útil).
Lo último puede escribirse también de este modo:
const esperar1Seg = () => new Promise(resolve => setTimeout(resolve, 1000));
Y entonces podemos contar de esta manera:
esperar1Seg()
.then(() => {
console.log(1);
return esperar1Seg();
})
.then(() => {
console.log(2);
return esperar1Seg();
})
.then(() => {
console.log(3);
});
Lo que estamos haciendo es llamar al método then
desde una promesa. Hacemos return esperar1Seg()
para de esta manera poder encadenar promesas.
Podemos hacer muchas cosas usando promesas. Todo es cuestión de práctica y creatividad.
Por ejemplo, podemos devolver "la promesa de imprimir un mensaje luego de 1 segundo".
const imprimirEn1Seg = (valor) => {
return new Promise(resolve => {
setTimeout(() => {
console.log(valor);
resolve();
}, 1000)
});
};
Y entonces prometemos continuamente ello en cadena:
imprimirEn1Seg(1)
.then(() => imprimirEn1Seg(2))
.then(() => imprimirEn1Seg(3));
Es importante el uso de then
sobre una promesa, porque de esta manera nos enteramos de su finalización.
Probablemente te estás preguntando si lo siguiente funcionaría:
imprimirEn1Seg(1)
.imprimirEn1Seg(2)
.imprimirEn1Seg(3);
Tal como tenemos ahora definida nuestra función imprimirEn1Seg
esto no funcionaría.
¿Por qué?
Porque la función devuelve una promesa. Este objeto Promise
no tiene acceso a un método imprimirEn1Seg
.
Es como si estuvieramos ejecutando esto: new Promise(/*...*/).imprimirEn1Seg(2)
. Y lo puedes verificar personalmente.
Pero estoy obsesionado con usar then sin usarlo.
Te entiendo. En teoría todo es posible.
En el mundo real tenemos muchas variables que no podemos controlar, e incluso algunas cosas suceden sin fundamento lógico.
Por lo menos en la programación, que es exacta, debemos procurar controlar todo lo que esté a nuestro alcance.
Entonces vamos a intentarlo.
¿Cómo podemos hacer que imprimirEn1Seg(1).imprimirEn1Seg(2).imprimirEn1Seg(3)
funcione?
imprimirEn1Seg(1)
devuelva un objeto que sí tenga acceso al método imprimirEn1Seg
.class Impresora extends Promise {
imprimirEn1Seg(valor) {
return this.then(() => this.devolverPromesa(valor));
}
devolverPromesa(valor) {
return new Impresora(resolve => {
setTimeout(() => {
console.log(valor);
resolve();
}, 1000)
})
}
}
Creamos una clase Impresora
a partir de la clase Promise
.
Como ahora el método imprimirEn1Seg
llama al método then
de la promesa, ya podemos usarlo de esta manera:
Impresora.resolve().imprimirEn1Seg(1).imprimirEn1Seg(2).imprimirEn1Seg(3);
¿Por qué hemos empezado con un resolve()
?
Hacemos esto porque si usamos new Impresora()
vamos a obtener un error (al heredar de Promise
la clase Impresora está obligada a recibir un executor
en su constructor).
¿Podemos sobreescribir el constructor? Sí, pero si existe una herencia de clases, de todas formas hay que llamar al constructor de la clase padre usando super
(es una regla de Javascript y no se puede ignorar).
¿Entonces no podemos hacer que imprimirEn1Seg(1).imprimirEn1Seg(2).imprimirEn1Seg(3)
funcione por sí solo?
Sí es posible encadenar promesas sin usar then
de forma explícita. Aunque debes tener en cuenta que usar then
no tiene nada de malo. De hecho, es bueno que te acostumbres a su uso.
La solución del último enlace requiere de bastante ingenio y no es del todo práctica. Pero lo comparto para que veas que todo es posible en un lenguaje muy flexible como Javascript.
Si estás interesado en continuar aprendiendo más de las últimas características de Javascript, te recomiendo inscribirte al curso que tengo publicado en Udemy. Aquí vemos muchos ejemplos, usando Javascript tal cual es (sin bibliotecas de terceros).
Comparte este post si te fue de ayuda 🙂.
Cualquier duda y/o sugerencia es bienvenida.
Regístrate
Inicia sesión para acceder a nuestros cursos y llevar un control de tu progreso.
Cursos recomendados
Curso intensivo. Incluye el desarrollo de una API, su consumo, y autenticación vía JWT. También vemos Kotlin desde 0.
Ingresar al cursoDomina JS con este curso práctico y completo! Fundamentos, ejemplos reales, ES6+, POO, Ajax, Webpack, NPM y más.
Ingresar al cursoAprende por qué es importante y cómo funciona Docker, con este nuevo curso práctico!
Ingresar al cursoInscríbete en nuestro curso gratuito de Laravel