Diferencia entre "promesa de devolución en espera" y "promesa de devolución"

Resuelto PitaJ asked hace 8 años • 7 respuestas

Dados los ejemplos de código siguientes, ¿existe 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 avatar Aug 02 '16 04:08 PitaJ
Aceptado

La mayoría de las veces, no hay diferencia observable entre returny return await. Ambas versiones delay1Secondtienen exactamente el mismo comportamiento observable (pero dependiendo de la implementación, la return awaitversión puede usar un poco más de memoria porque Promisese puede crear 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 asíncrona espera la promesa rechazada antes de devolver su resultado, lo que hace que el rechazo se convierta en una excepción y catchse alcance la cláusula; Por tanto, la función devolverá una promesa que se resolverá 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 nocatch se llama al caso y la persona que llama recibe el rechazo.

Denis Washington avatar Mar 12 '2017 17:03 Denis Washington

Como se mencionó en otras respuestas, es probable que exista un ligero beneficio de rendimiento al dejar que la promesa surja al devolverla directamente, simplemente porque no es necesario esperar el resultado primero y luego envolverlo con otra promesa nuevamente. Sin embargo, nadie ha hablado todavía de la optimización de las llamadas de cola .

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, no muchos tiempos de ejecución lo admiten todavía , 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 otra función devuelve directamente. En cambio, reutiliza el mismo marco.

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

Dado que delay()lo devuelve directamente 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án el mismo marco que se abrió para la función externa. Esto optimiza la pila porque puede evitar un desbordamiento de la pila (jeje) con funciones recursivas muy grandes, 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 devolverlo, 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 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: ¿Existen optimizaciones para llamadas finales en funciones asíncronas?

chharvey avatar Mar 09 '2019 19:03 chharvey

Diferencia notable: el rechazo de la promesa se maneja en diferentes lugares

  • return somePromisepasará algunaPromise al sitio de la llamada y await algunaPromise para liquidar en el sitio de la llamada (si hay alguna). Por lo tanto, si se rechaza alguna Promesa, no será manejada por el bloque catch local, sino por el bloque catch 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'
Expandir fragmento

  • return await somePromisePrimero esperará alguna promesa para establecerse localmente. Por lo tanto, el valor o la excepción se manejarán primero localmente. => El bloque catch 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'
Expandir fragmento

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. Llamémoslo delayPromise.
async function delay1Second() {
  return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. Las funciones asíncronas envolverán su valor de retorno dentro 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 se delayPromiseliquide.
  • SI delayPromisese cumple con PromiseValue=1:
const result = 1; 
  • ELSE es delayPromiserechazado:
// jump to catch block if there is any

regreso aguarda 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. Llamémoslo delayPromise.
async function delay1Second() {
  return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
  1. La espera local esperará hasta que delayPromisese resuelva.
  • 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]]cambia de pendinga resolvedorejected
Ragtime avatar Jul 09 '2020 16:07 Ragtime

En nuestro proyecto, decidimos utilizar siempre "espera de retorno". El argumento es que "el riesgo de olvidar agregar 'await' cuando más adelante se coloca un bloque try-catch alrededor de la expresión de retorno justifica tener el 'await' redundante ahora".

MickH avatar Dec 02 '2021 11:12 MickH

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

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

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

Entonces, 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 avatar Aug 01 '2016 21:08 nrabinowitz