Diferencia entre `retorno en espera de promesa` y` retorno de promesa`

106

Dados los ejemplos de código a continuación, ¿hay alguna diferencia en el comportamiento y, de ser así, cuáles son esas diferencias?

return await promise

async function delay1Second() {
  return (await delay(1000));
}

return promise

async function delay1Second() {
  return delay(1000);
}

Según tengo entendido, el primero tendría manejo de errores dentro de la función asíncrona, y los errores saldrían de la Promesa de la función asíncrona. Sin embargo, el segundo requeriría un tic menos. ¿Es esto correcto?

Este fragmento es solo una función común para devolver una Promesa como referencia.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}
PitaJ
fuente
3
Sí, edité mi pregunta porque no entendiste lo que quería decir y realmente no respondió lo que me preguntaba.
PitaJ
1
@PitaJ: Creo que querías eliminar el asyncde tu segundo ( return promise) ejemplo.
Stephen Cleary
1
@PitaJ: En ese caso, su segundo ejemplo devolvería una promesa que se resuelve con una promesa. Más bien extraño.
Stephen Cleary
5
jakearchibald.com/2017/await-vs-return-vs-return-await es un buen artículo que resume las diferencias
sanchit
2
@StephenCleary, me topé con esto y primero pensé exactamente lo mismo, una promesa que se resuelve con una promesa no tiene sentido aquí. Pero a medida que gira, promise.then(() => nestedPromise)aplanaría y "seguiría" el nestedPromise. Es interesante cómo es diferente de las tareas anidadas en C # donde tendríamos que Unwraphacerlo. En una nota al margen, parece que las await somePromise llamadas Promise.resolve(somePromise).then, en lugar de solo somePromise.then, tienen algunas diferencias semánticas interesantes.
noseratio

Respuestas:

152

La mayoría de las veces, no hay una diferencia observable entre returny return await. Ambas versiones de delay1Secondtienen exactamente el mismo comportamiento observable (pero dependiendo de la implementación, la return awaitversión podría usar un poco más de memoria porque Promisepodría crearse un objeto intermedio ).

Sin embargo, como señaló @PitaJ, hay un caso en el que hay una diferencia: si returno return awaitestá anidado en un bloque try- catch. Considere este ejemplo

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

En la primera versión, la función async espera la promesa rechazada antes de devolver su resultado, lo que provoca que el rechazo se convierta en una excepción y catchse alcance la cláusula; la función devolverá así una promesa que se resuelve en la cadena "¡Guardado!".

La segunda versión de la función, sin embargo, devuelve la promesa rechazada directamente sin esperarla dentro de la función asíncrona , lo que significa que no se llama al catchcaso y la persona que llama obtiene el rechazo.

Denis Washington
fuente
¿Quizás también mencionar que el seguimiento de la pila sería diferente (incluso sin un intento / captura)? Creo que ese es el problema que la gente encuentra con más frecuencia en este ejemplo:]
Benjamin Gruenbaum
He descubierto en un escenario que usar return new Promise(function(resolve, reject) { })dentro de un for...ofbucle y luego llamar resolve()dentro del bucle después de pipe()que no pausa la ejecución del programa hasta que la tubería se haya completado, como se desea, sin embargo, el uso await new Promise(...)sí lo hace. ¿Es esta última incluso una sintaxis válida / correcta? es 'taquigrafía' para return await new Promise(...)? ¿Podrías ayudarme a entender por qué funciona el último y el primero no? para el contexto, el escenario es en el solution 02de esta respuesta
user1063287
10

Como se mencionó en otras respuestas, es probable que exista un ligero beneficio en el rendimiento al dejar que la promesa brote devolviéndola directamente, simplemente porque no tiene que esperar el resultado primero y luego envolverlo con otra promesa nuevamente. Sin embargo, nadie ha hablado todavía sobre la optimización de llamadas finales .

La optimización de llamadas de cola , o "llamadas de cola adecuadas" , es una técnica que utiliza el intérprete para optimizar la pila de llamadas. Actualmente, todavía no hay muchos tiempos de ejecución que lo admitan, aunque técnicamente es parte del estándar ES6 , pero es posible que se agregue soporte en el futuro, por lo que puede prepararse para eso escribiendo un buen código en el presente.

En pocas palabras, TCO (o PTC) optimiza la pila de llamadas al no abrir un nuevo marco para una función que es devuelta directamente por otra función. En cambio, reutiliza el mismo marco.

async function delay1Second() {
  return delay(1000);
}

Dado que delay()es devuelto directamente por delay1Second(), los tiempos de ejecución que admiten PTC primero abrirán un marco para delay1Second()(la función externa), pero luego, en lugar de abrir otro marco para delay()(la función interna), simplemente reutilizará el mismo marco que se abrió para la función externa. Esto optimiza la pila ya que puede evitar un desbordamiento de pila (hehe) con muy grandes funciones recursivas, por ejemplo, fibonacci(5e+25). Básicamente, se convierte en un bucle, que es mucho más rápido.

PTC solo se habilita cuando la función interna se devuelve directamente . No se usa cuando el resultado de la función se modifica antes de que se devuelva, por ejemplo, si tenía return (delay(1000) || null), o return await delay(1000).

Pero como dije, la mayoría de los tiempos de ejecución y los navegadores aún no son compatibles con PTC, por lo que probablemente no haga una gran diferencia ahora, pero no estaría de más preparar su código para el futuro.

Lea más en esta pregunta: Node.js: ¿Hay optimizaciones para llamadas finales en funciones asíncronas?

chharvey
fuente
2

Esta es una pregunta difícil de responder, porque depende en la práctica de cómo su transpiler (probablemente babel) se procese realmente async/await. Las cosas que están claras independientemente:

  • Ambas implementaciones deben comportarse igual, aunque la primera implementación puede tener una menos Promiseen la cadena.

  • Especialmente si descarta lo innecesario await, la segunda versión no requeriría ningún código adicional del transpilador, mientras que la primera sí.

Por lo tanto, desde una perspectiva de depuración y rendimiento del código, la segunda versión es preferible, aunque solo un poco, mientras que la primera versión tiene un ligero beneficio de legibilidad, ya que indica claramente que devuelve una promesa.

nrabinowitz
fuente
¿Por qué las funciones se comportarían igual? El primero devuelve un valor resuelto ( undefined) y el segundo devuelve un Promise.
Amit
4
@Amit ambas funciones devuelven una promesa
PitaJ
Ack. Es por eso que no puedo soportarlo async/await, me resulta mucho más difícil razonar. @PitaJ es correcto, ambas funciones devuelven una Promesa.
nrabinowitz
¿Qué pasa si rodeo el cuerpo de ambas funciones asíncronas con un try-catch? En el return promisecaso, cualquiera rejectionno sería capturado, ¿correcto, mientras que, en el return await promisecaso, sería correcto?
PitaJ
Ambos devuelven una Promesa, pero el primero "promete" un valor primitivo y el segundo "promete" una Promesa. Si awaitcada uno de estos en algún sitio de llamada, el resultado será muy diferente.
Amit
0

aqui te dejo un codigo practico para que puedas entenderlo la diferencia

 let x = async function () {
  return new Promise((res, rej) => {
    setTimeout(async function () {
      console.log("finished 1");
      return await new Promise((resolve, reject) => { // delete the return and you will see the difference
        setTimeout(function () {
          resolve("woo2");
          console.log("finished 2");
        }, 5000);
      });
      res("woo1");
    }, 3000);
  });
};

(async function () {
  var counter = 0;
  const a = setInterval(function () { // counter for every second, this is just to see the precision and understand the code
    if (counter == 7) {
      clearInterval(a);
    }

    console.log(counter);
    counter = counter + 1;
  }, 1000);
  console.time("time1");
  console.log("hello i starting first of all");
  await x();
  console.log("more code...");
  console.timeEnd("time1");
})();

la función "x" solo es una función asíncrona que tiene otras funciones si se borrará el retorno imprime "más código ..."

la variable x es solo una función asincrónica que a su vez tiene otra función asincrónica, en la principal del código invocamos una espera para llamar a la función de la variable x, cuando se completa sigue la secuencia del código, eso sería normal para "async / await", pero dentro de la función x hay otra función asincrónica, y esta devuelve una promesa o devuelve una "promesa" permanecerá dentro de la función x, olvidando el código principal, es decir, no imprimirá el "console.log (" más código .. "), en cambio si ponemos" await "esperará cada función que se complete y finalmente siga la secuencia normal del código principal.

debajo de "console.log (" terminado 1 "elimina el" retorno ", verás el comportamiento.

Carlos Terrazas
fuente
1
Si bien este código puede resolver la pregunta, incluir una explicación de cómo y por qué esto resuelve el problema realmente ayudaría a mejorar la calidad de su publicación y probablemente resultaría en más votos a favor. Recuerde que está respondiendo la pregunta a los lectores en el futuro, no solo a la persona que pregunta ahora. Por favor, editar su respuesta para agregar explicaciones y dar una indicación de lo que se aplican limitaciones y supuestos.
Brian
0

Aquí hay un ejemplo mecanografiado que puede ejecutar y convencerse de que necesita ese "retorno en espera"

async function  test() {
    try {
        return await throwErr();  // this is correct
        // return  throwErr();  // this will prevent inner catch to ever to be reached
    }
    catch (err) {
        console.log("inner catch is reached")
        return
    }
}

const throwErr = async  () => {
    throw("Fake error")
}


void test().then(() => {
    console.log("done")
}).catch(e => {
    console.log("outer catch is reached")
});

David Dehghan
fuente
0

Diferencia notable: el rechazo de promesas se maneja en diferentes lugares

  • return somePromisepasará somePromise al sitio de la llamada y await somePromise para asentarse en el sitio de la llamada (si hay alguno). Por lo tanto, si se rechaza somePromise, no será manejado por el bloque de captura local, sino por el bloque de captura del sitio de llamada.

async function foo () {
  try {
    return Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'OUT'

  • return await somePromisePrimero esperará alguna promesa de establecerse localmente. Por lo tanto, el valor o la excepción primero se manejarán localmente. => El bloque de captura local se ejecutará si somePromisese rechaza.

async function foo () {
  try {
    return await Promise.reject();
  } catch (e) {
    console.log('IN');
  }
}

(async function main () {
  try {
    let a = await foo();
  } catch (e) {
    console.log('OUT');
  }
})();
// 'IN'

Razón: return await Promiseespera tanto localmente como afuera, return Promiseespera solo afuera

Pasos detallados:

promesa de devolución

async function delay1Second() {
  return delay(1000);
}
  1. llamar delay1Second();
const result = await delay1Second();
  1. En el interior delay1Second(), la función delay(1000)devuelve una promesa inmediatamente con [[PromiseStatus]]: 'pending. Vamos a llamarlo delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Las funciones asíncronas envolverán su valor de retorno dentro de Promise.resolve()( Fuente ). Como delay1Secondes una función asíncrona, tenemos:
const result = await Promise.resolve(delayPromise); 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. Promise.resolve(delayPromise)regresa delayPromisesin hacer nada porque la entrada ya es una promesa (ver MDN Promise.resolve ):
const result = await delayPromise; 
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
  1. awaitespera hasta que delayPromisese resuelva.
  • SI delayPromisese cumple con PromiseValue = 1:
const result = 1; 
  • ELSE delayPromisees rechazado:
// jump to catch block if there is any

regreso a la espera de la promesa

async function delay1Second() {
  return await delay(1000);
}
  1. llamar delay1Second();
const result = await delay1Second();
  1. En el interior delay1Second(), la función delay(1000)devuelve una promesa inmediatamente con [[PromiseStatus]]: 'pending. Vamos a llamarlo delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. La espera local esperará hasta que delayPromisese establezca.
  • Caso 1 : delayPromisese cumple con PromiseValue = 1:
async function delay1Second() {
  return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise; 
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1; 
  • Caso 2 : delayPromisese rechaza:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;

Glosario:

  • Liquidar: Promise.[[PromiseStatus]]cambios de pendinga resolvedorejected
Rag-time
fuente