Promises (Promesas) en Javascript

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.

  • Usas esta nueva característica de Javascript en tu día a día. En este caso, un repaso no te vendría nada mal.

¿Por qué son importantes las Promesas en JS?

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 no son siempre la mejor opción

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:

  • Una promesa solo puede tener éxito o fracasar una única vez. No puede tener éxito o fallar por una 2da vez, ni cambiar de éxito a fallo posteriormente, o viceversa.
  • Si una promesa ha sido exitosa o ha fallado, y más adelante (recién) registramos un 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.

Terminología asociada a las Promesas

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 éxito
  • rejected - 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).

Promesas en cadena (Chaining)

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:

  • La función "hacerAlgo" registra un callback. Esta función se llamará posteriormente (tras hacer algo), y recibirá un parámetro "resultado".
  • Este valor que se recibe se usa como argumento al llamar a "hacerAlgoMas", que termina definiendo una segunda función de callback (que espera un "nuevoResultado").
  • Este nuevo resultado se requiere para iniciar la tercera operación. La función callback que define "hacerUnaTerceraCosa" es la última en llamarse (si sale todo bien), e imprime por consola el resultado final.
  • Si alguna función, de las 3 que se llamaron, falla, entonces se invoca su correspondiente 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);

Entonces, ¿qué son las Promesas?

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.

Cómo crear una Promesa en JS

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.

  • El valor devuelto por la función 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:

  • Se ejecuta la función executor y se crea nuestro objeto Promise.
  • Se llama al método then, expresando qué es lo que queremos hacer con el valor que devolverá la promesa.
  • Se imprime por consola [object Promise].
  • A los 3 segundos tras ejecutar la función 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?

  • La idea es que imprimirEn1Seg(1) devuelva un objeto que sí tenga acceso al método imprimirEn1Seg.
  • Una alternativa de solución consiste en definir una clase que tenga este método disponible.
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).

Referencias

promises callbacks events

Cursos recomendados

Curso intensivo de Laravel y Android

Laravel y Android

Curso intensivo que incluye el desarrollo de una API, su consumo, y autenticación vía JWT. También vemos Kotlin desde 0.

Ver más
Curso práctico de Javascript

Aprende Javascript

Domina JS con este curso práctico y completo! Fundamentos, ejemplos reales, ES6+, POO, Ajax, Webpack, NPM y más.

Ver más
Curso de Laravel, Vue.js y Pusher

Aprende Vue.js

Desarrollemos un Messenger! Aprende sobre Channels, Queues, Vuex, JWT, Sesiones, BootstrapVue y mucho más.

Ver más
Logo de Programación y más

¿Tienes alguna duda?

Si algo no te quedó claro o tienes alguna sugerencia, escribe un comentario aquí debajo.

Además recuerda compartir el post si te resultó de ayuda. Gracias!

Antes que te vayas

Inscríbete en nuestro curso gratuito de Laravel