¿Qué es una promesa en JavaScript?

JavaScript tiene un hilo de proceso único, lo que significa que dos porciones del script no pueden ejecutarse al mismo tiempo, deben hacerlo una después de la otra. Una promesa es un objeto que representa la terminación o el fracaso de una operación asíncrona, y el valor resultante.

var promise = new Promise(function(resolve, reject) {
  // hacemos algo, entonces…

  if (/* todo funciono */) {
    resolve("Mira, funcionó!");
  }
  else {
    reject(Error("Se rompe"));
  }
});

Una Promesa existe en uno de estos estados

  • Pendiente: Estado inicial, ni cumplida ni rechazada.
  • Cumplida: la operación se completó satisfactoriamente.
  • Rechazada: la operación falló.

El objeto promesa funciona como un contenedor para un valor que no necesariamente conocemos cuando la promesa es creada. En una operación asíncrona, nos permite asociar distintos controladores al resultado si se cumple con éxito, o a los motivos del error si ha fallado.

Esto le permite a un método asíncrono devolver un valor de manera síncrona: en lugar de devolver el valor final, devuelve una promesa de proporcionar ese valor en algún momento del futuro.

Usando ‘Then’ para encadenar promesas

Para tomar varias llamadas asíncronas y sincronizarlas una luego de la otra, podeos utilizar el encadenamiento de promesas. Esto nos permite utilizar el valor resultante de una promesa en subsecuentes callbacks.

Promise.resolve('alguna')
  .then(function(string) { // <-- Esto sucederá luego de que se resuelva la promesa anterior (devolviendo el valor 'alguna')
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
        string += ' cosa';
        resolve(string);
      }, 1);
    });
  })
  .then(function(string) { // <-- Esto sucederá luego de que la nueva promesa del .then anterior se resuelva
    console.log(string); // <-- Se muestra 'alguna cosa' en la consola
  });

Promesas API

Hay cuatro métodos estáticos en la clase Promesa:

  • Promise.resolve
  • Promise.reject
  • Promise.all
  • Promise.race

Las promesas pueden encadenarse

Cuando escribimos promesas para resolver un problema particular, podemos encadenarlas para crear una lógica.

var add = function(x, y) {
  return new Promise((resolve,reject) => {
    var sum = x + y;
    if (sum) {
      resolve(sum);
    }
    else {
      reject(Error("No se pudo realizar la suma!"));
    }
  });
};

var subtract = function(x, y) {
  return new Promise((resolve, reject) => {
    var sum = x - y;
    if (sum) {
      resolve(sum);
    }
    else {
      reject(Error("No se pudo realizar la resta!"));
    }
  });
};

// Empezando cadena de promesas
add(2,2)
  .then((added) => {
    // suma = 4
    return subtract(added, 3);
  })
  .then((subtracted) => {
    // resta = 1
    return add(subtracted, 5);
  })
  .then((added) => {
    // suma = 6
    return added * 2;    
  })
  .then((result) => {
    // resultado = 12
    console.log("My result is ", result);
  })
  .catch((err) => {
    // Si alguna parte de la cadena es rechazada, devolver mensaje de error.
    console.log(err);
  });

Esto es útil para seguir un paradigma de programación funcional. Al crear funciones para manipular datos, podemos encadenarlas para ensamblar un resultado final. Si en cualquier punto de la cadena de funciones se rechaza un valor, la cadena saltará al controlador catch() más cercano.

Para más información sobre Programación Funcional: Programación Funcional

Generadores de Funciones

En versiones recientes, JavaScript introdujo nuevas formas de manejar Promesas de manera nativa. Una de ellas son los generadores de funciones. Estos generadores son funciones que pueden ser pausadas. Al ser usados con Promesas, los generadores hacen su lectura más fácil y de apariencia síncrona.

const myFirstGenerator = function* () {
  const one = yield 1;
  const two = yield 2;
  const three = yield 3;

  return 'Terminado!';
}

const gen = myFirstGenerator();

Aquí tenemos nuestro primer generador, que podemos identificar por la sintaxis function*. La variable gen no va a ejecutar myFirstGenerator directamente, pero alistará al generador para ser usado.

console.log(gen.next());
// Returns { value: 1, done: false }

Al ejecutar gen.next() se reanudará el generador. Como es la primera vez que llamamos gen.next() ejecutará yield 1 y volverá a pausarse hasta que volvamos a llamar gen.next(). Cuando yield 1 es llamado, nos devolverá el value que se obtuvo y si el generador está hecho (done) o no.

console.log(gen.next());
// Devuelve { value: 2, done: false }

console.log(gen.next());
// Devuelve { value: 3, done: false }

console.log(gen.next());
// Devuelve { value: 'Terminado!', done: true }

console.log(gen.next());
// Arroja un error

A medida que seguimos llamando gen.next() continuará hacia el siguiente yield y se pausará cada vez. Cuando se terminen los yield, procederá a ejecutar el resto del generador, que en este caso solo devolver 'Terminado!'. Si llamamos gen.next() una vez más, obtendremos un error porque el generador ha concluido.

Ahora bien, imaginemos que cada yield en este ejemplo es una promesa, en ese caso el código luciría extremadamente síncrono.

Promise.all(iterable) es muy útil para múltiples consultas a distintas fuentes

El método Promise.all (iterable) devuelve una única Promesa que se resolverá cuando todas las promesas del argumento iterable se hayan resuelto o cuando el argumento iterable no contenga promesas. Rechaza con la razón de la primera promesa que rechaza.

var promise1 = Promise.resolve(catSource);
var promise2 = Promise.resolve(dogSource);
var promise3 = Promise.resolve(cowSource);

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});
// resultado esperado: Array ["catData", "dogData", "cowData"]

Más información sobre promesas:

Traducido del artículo - JavaScript Promises Explained