Esta traducción está incompleta. Por favor, ayuda a traducir este artículo del inglés.

Una Promise (promesa en castellano) es un objeto que representa la terminación o el fracaso eventual de una operación asíncrona. Una promesa puede ser creada usando su constructor. Sin embargo, la mayoría de la gente son consumidores de promesas ya creadas devueltas desde funciones. Esta guía explorará por lo tanto la consumición (el uso) de promesas devueltas primero.

Esencialmente, una promesa es un objeto devuelto al cual enganchas las funciones callback, en vez de pasar funciones callback a una función.

Por ejemplo, en vez de una función del viejo estilo que espera dos funciones callback, y llama a una de ellas en caso de terminación o fallo:

function exitoCallback(resultado) {
  console.log("Tuvo éxito con " + resultado);
}

function falloCallback(error) {
  console.log("Falló con " + error);
}

hazAlgo(exitoCallback, falloCallback);

... las funciones modernas devuelven una promesa a la que puedes enganchar tus funciones de retorno:

let promesa = hazAlgo(); 
promesa.then(exitoCallback, falloCallback);

…o simplemente:

hazAlgo().then(exitoCallback, falloCallback);

Nosotros llamamos a esto una llamada a función asíncrona. Esta convención tiene varias ventajas. Exploraremos cada una de ellas.

Garantías

A diferencia de las funciones callback pasadas al viejo estilo, una promesa viene con algunas garantías:

  • Las funciones callback nunca serán llamadas antes de la terminación de la ejecución actual del bucle de eventos de JavaScript.
  • Las funciones callback añadidas con .then serán llamadas después del éxito o fracaso de la operación asíncrona, como arriba.
  • Pueden ser añadidas múltiples funciones callback llamando a .then varias veces, para ser ejecutadas independientemente en el orden de inserción.

Pero el beneficio más inmediato de las promesas es el encadenamiento.

Encadenamiento

Una necesidad común es el ejecutar dos o más operaciones asíncronas seguidas, donde cada operación posterior se inicia cuando la operación previa tiene éxito, con el resultado del paso previo. Logramos esto creando una cadena de promesas.

Aquí está la magia: la función then devuelve una promesa nueva, diferente de la original:

const promesa = hazAlgo();
const promesa2 = promesa.then(exitoCallback, falloCallback);

o

let promesa2 = hazAlgo().then(exitoCallback, falloCallback);

Esta segunda promesa representa la terminación no sólo de hazAlgo(), sino también de exitoCallback o falloCallback que pasaste, las cuales pueden ser otras funciones asíncronas devolviendo una promesa. Cuando ese es el caso, cualquier función callback añadida a promesa2 se queda encolada detrás de la promesa devuelta por exitoCallback o falloCallback.

Básicamente, cada promesa representa la terminación de otro paso asíncrono en la cadena.

En los viejos días, hacer varias operaciones asíncronas en fila conduciría a la clásica pirámide maldita de funciones callback:

hazAlgo(function(resultado) {
  hazAlgoMas(resultado, function(nuevoResultado) {
    hazLaTerceraCosa(nuevoResultado, function(resultadoFinal) {
      console.log('Obtenido el resultado final: ' + resultadoFinal
    }, falloCallback);
  }, falloCallback);
}, falloCallback);

Con las funciones modernas, en cambio, enganchamos nuestras funciones callback a las promesas devueltas, formando una cadena de promesas:

hazAlgo().then(function(resultado) {
  return hazAlgoMas(resultado);
})
.then(function(nuevoResultado) {
  return hazLaTerceraCosa(nuevoResultado);
})
.then(function(resultadoFinal) {
  console.log('Obtenido el resultado final: ' + resultadoFinal);
})
.catch(falloCallback);

Los argumentos a then son opcionales, y catch(falloCallBack) es un atajo para then(null, falloCallBack). Es posible que vea esto expresado con funciones de flecha en su lugar:

hazAlgo()
.then(resultado => hazAlgoMas(resultado))
.then(nuevoResultado => hazLaTerceraCosa(nuevoResultado))
.then(resultadoFinal => {
  console.log(`Obtenido el resultado final: ${resultadoFinal}`);
})
.catch(falloCallback);

Importante: Siempre devuelva promesas, de otra forma las funciones callback no se encadenarán, y los errores no sería capturados.

Encadenar después de una captura

Es posible encadenar después de un fallo, por ejemplo, un catch, lo que es útil para lograr nuevas acciones incluso después de una acción falló en la cadena. Lea el siguiente ejemplo:

new Promise((resolver, rechazar) => {
    console.log('Inicial');

    resolver();
})
.then(() => {
    throw new Error('Algo falló');
        
    console.log('Haz esto');
})
.catch(() => {
    console.log('Haz eso');
})
.then(() => {
    console.log('Haz esto sin que importe lo que sucedió antes');
});

Esto devolverá el siguiente texto:

Inicial
Haz eso
Haz esto sin que importe lo que sucedió antes

Note que el texto "Haz esto" no es escrito porque el error "Algo falló" causó un rechazo.

Propagación de errores

Usted podría recordar ver falloCallback tres veces en la pirámide maldita de antes, en comparación con sólo una vez al final de la cadena de promesas:

hazAlgo()
.then(resultado => hazAlgoMas(valor))
.then(nuevoResultado => hazLaTerceraCosa(nuevoResultado))
.then(resultadoFinal => console.log(`Obtenido el resultado final: ${resultadoFinal}`))
.catch(falloCallback);

Básicamente, una cadena de promesas se para si hay una excepción, recorriendo la cadena por manejadores de captura en su lugar. Esto está modelado a la forma como trabaja el código síncrono:

try {
  let resultado = syncHazAlgo();
  let nuevoResultado = syncHazAlgoMas(resultado);
  let resultadoFinal = syncHazLaTerceraCosa(nuevoResultado);
  console.log(`Obtenido el resultado final: ${resultadoFinal}`);
} catch(error) {
  falloCallback(error);
}

Esta simetría con el código síncrono culmina con la mejora sintáctica async/await en ECMASCript 2017:

async function foo() {
  try {
    let resultado = await hazAlgo();
    let nuevoResultado = await hazAlgoMas(resultado);
    let resultadoFinal = await hazLaTerceraCosa(nuevoResultado);
    console.log(`Obtenido el resultado final: ${resultadoFinal}`);
  } catch(error) {
    falloCallback(error);
  }
}

Se construye sobre promesas, por ejemplo, hazAlgo() es la misma función que antes. Puedes leer más sobre la sintaxis aquí.

Las promesas resuelven un fallo fundamental de la pirámide maldita de funciones callback, capturando todos los errores, incluso excepciones lanzadas y errores de programación. Esto es esencial para la composición funcional de operaciones asíncronas.

Creating a Promise around an old callback API

A Promise can be created from scratch using its constructor. This should be needed only to wrap old APIs.

In an ideal world, all asynchronous functions would already return promises. Alas, some APIs still expect success and/or failure callbacks to be passed in the old way. The quintessential example is the setTimeout() function:

setTimeout(() => saySomething("10 seconds passed"), 10000);

Mixing old-style callbacks and promises is problematic. If saySomething fails or contains a programming error, nothing catches it.

Luckily we can wrap it in a promise. Best practice is to wrap problematic functions at the lowest possible level, and then never call them directly again:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout doesn't really fail, we left out reject in this case.

Composition

Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.

Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

Sequential composition is possible using some clever JavaScript:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

Basically, we reduce an array of asynchronous functions down to a promise chain equivalent to: Promise.resolve().then(func1).then(func2);

This can also be done with a reusable compose function, which is common in functional programming:

let applyAsync = (acc,val) => acc.then(val);
let composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

The composeAsync function will accept any number of functions as arguments, and will return a new function that accepts an initial value to be passed through the composition pipeline. This is beneficial because any or all of the functions may be either asynchronous or synchronous, and they are guaranteed to be executed in the correct order:

let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

In ECMAScript 2017, sequential composition can be done more simply with async/await:

for (let f of [func1, func2]) {
  await f();
}

Timing

To avoid surprise, functions passed to then will never be called synchronously, even with an already-resolved promise:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop, i.e. pretty soon:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

See also

Etiquetas y colaboradores del documento

 Colaboradores en esta página: hamfree
 Última actualización por: hamfree,