¿Debo regresar después de una resolución / rechazo temprano?

262

Supongamos que tengo el siguiente código.

function divide(numerator, denominator) {
 return new Promise((resolve, reject) => {

  if(denominator === 0){
   reject("Cannot divide by 0");
   return; //superfluous?
  }

  resolve(numerator / denominator);

 });
}

Si mi objetivo es usar rejectpara salir temprano, ¿debo adquirir el hábito de hacerlo también returninmediatamente después?

sam
fuente
55
Sí, debido a la ejecución hasta la finalización

Respuestas:

371

El returnpropósito es terminar la ejecución de la función después del rechazo y evitar la ejecución del código después de ella.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {

    if (denominator === 0) {
      reject("Cannot divide by 0");
      return; // The function execution ends here 
    }

    resolve(numerator / denominator);
  });
}

En este caso, evita resolve(numerator / denominator);que se ejecute, lo cual no es estrictamente necesario. Sin embargo, aún es preferible terminar la ejecución para evitar una posible trampa en el futuro. Además, es una buena práctica evitar que se ejecute código innecesariamente.

Antecedentes

Una promesa puede ser en uno de los 3 estados:

  1. pendiente - estado inicial. De pendiente podemos pasar a uno de los otros estados
  2. cumplido - operación exitosa
  3. rechazado - operación fallida

Cuando una promesa se cumple o se rechaza, permanecerá en este estado indefinidamente (resuelta). Por lo tanto, rechazar una promesa cumplida o cumplir una promesa rechazada no tendrá efecto.

Este fragmento de ejemplo muestra que, aunque la promesa se cumplió después de ser rechazada, se mantuvo rechazada.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }

    resolve(numerator / denominator);
  });
}

divide(5,0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Entonces, ¿por qué necesitamos volver?

Aunque no podemos cambiar un estado de promesa establecido, rechazar o resolver no detendrá la ejecución del resto de la función. La función puede contener código que creará resultados confusos. Por ejemplo:

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) {
      reject("Cannot divide by 0");
    }
    
    console.log('operation succeeded');

    resolve(numerator / denominator);
  });
}

divide(5, 0)
  .then((result) => console.log('result: ', result))
  .catch((error) => console.log('error: ', error));

Incluso si la función no contiene ese código en este momento, esto crea una posible trampa futura. Un futuro refactor podría ignorar el hecho de que el código aún se ejecuta después de que se rechaza la promesa, y será difícil de depurar.

Detener la ejecución después de resolver / rechazar:

Este es el flujo de control estándar de JS.

  • Regresar después de resolve/ reject:

  • Regrese con el resolve/ reject- ya que se ignora el valor de retorno de la devolución de llamada, podemos guardar una línea devolviendo la declaración de rechazo / resolución:

  • Usando un bloque if / else:

Prefiero usar una de las returnopciones ya que el código es más plano.

Ori Drori
fuente
28
Vale la pena señalar que el código no se comportará de manera diferente si returnestá allí o no, porque una vez que se ha establecido un estado de promesa, no se puede cambiar, por lo que llamar resolve()después de llamar reject()no hará nada excepto usar unos pocos ciclos de CPU adicionales. Yo mismo usaría el returnpunto de vista de la limpieza y la eficiencia del código justo, pero no es obligatorio en este ejemplo específico.
jfriend00
1
Intente usar en Promise.try(() => { })lugar de una nueva Promesa y evite usar llamadas de resolución / rechazo. En su lugar, podría escribir return denominator === 0 ? throw 'Cannot divide by zero' : numerator / denominator; I use Promise.trycomo medio para iniciar una Promesa y eliminar promesas envueltas en bloques try / catch que son problemáticos.
kingdango
2
Es bueno saberlo, y me gusta el patrón. Sin embargo, en este momento Promise.try es una sugerencia de etapa 0, por lo que solo puede usarlo con una cuña o usando una biblioteca de promesas como bluebird o Q.
Ori Drori
66
@ jfriend00 Obviamente, en este sencillo ejemplo, el código no se comportará de manera diferente. Pero, ¿qué pasaría si tuviera código después de rejecteso que hace algo costoso, como conectarse a bases de datos o puntos finales API? Todo sería innecesario y le costaría dinero y recursos, especialmente, por ejemplo, si se conecta a algo como una base de datos de AWS o un punto final de API Gateway. En ese caso, definitivamente usaría un retorno para evitar que se ejecute código innecesario.
Jake Wilson
3
@JakeWilson: por supuesto, eso es solo un flujo de código normal en Javascript y no tiene nada que ver con las promesas. Si ha terminado de procesar la función y no desea que se ejecute más código en la ruta de código actual, inserte un return.
jfriend00
37

Un modismo común, que puede o no ser su taza de té, es combinar el returncon el reject, rechazar simultáneamente la promesa y salir de la función, de modo que el resto de la función, incluido el resolveno se ejecute. Si te gusta este estilo, puede hacer que tu código sea un poco más compacto.

function divide(numerator, denominator) {
  return new Promise((resolve, reject) => {
    if (denominator === 0) return reject("Cannot divide by 0");
                           ^^^^^^^^^^^^^^
    resolve(numerator / denominator);
  });
}

Esto funciona bien porque el constructor promesa no hace nada con cualquier valor de retorno, y en todo caso resolvey rejectde retorno nada.

El mismo idioma se puede usar con el estilo de devolución de llamada que se muestra en otra respuesta:

function divide(nom, denom, cb){
  if(denom === 0) return cb(Error("Cannot divide by zero"));
                  ^^^^^^^^^
  cb(null, nom / denom);
} 

Nuevamente, esto funciona bien porque la persona que llama divideno espera que devuelva nada y no hace nada con el valor de retorno.


fuente
66
No me gusta esto. Esto da la noción de que estás devolviendo algo que de hecho no estás. Invocas la función de rechazo y luego usas return para finalizar la ejecución de la función. Mantenlos en líneas separadas, lo que estás haciendo solo confundirá a las personas. La legibilidad del código es el rey.
K - La toxicidad en SO está creciendo.
77
@KarlMorrison, de hecho, estás devolviendo "algo", una promesa rechazada. Creo que esa "noción" de la que estás hablando es muy personal. No hay nada de malo en devolver un rejectestado
Frondor
55
@Frondor No creo que hayas entendido lo que escribí. Por supuesto, usted y yo entendemos esto, no sucede nada al devolver un rechazo en la misma línea. Pero, ¿qué pasa con los desarrolladores que no están tan acostumbrados a que JavaScript ingrese a un proyecto? Este tipo de programación disminuye la legibilidad para esas personas. El ecosistema JavaScript de hoy es un desastre y las personas que difunden este tipo de prácticas solo lo empeorarán. Esta es una mala práctica.
K - La toxicidad en SO está creciendo.
1
@KarlMorrison ¡Opiniones personales! = Mala práctica. Probablemente ayudaría a un nuevo desarrollador de Javascript a comprender lo que está sucediendo con el retorno.
Toby Caulk
1
@TobyCaulk Si la gente necesita saber qué hace el retorno, entonces no deberían estar jugando con Promesas, deberían estar aprendiendo programación básica.
K - La toxicidad en SO está creciendo.
10

Técnicamente no es necesario aquí 1 , porque una Promesa puede resolverse o rechazarse, exclusivamente y solo una vez. El primer resultado de Promise gana y cada resultado posterior se ignora. Esto es diferente de las devoluciones de llamada de estilo de nodo.

Dicho esto, es una buena práctica limpia asegurarse de llamar exactamente a uno, cuando sea práctico, y de hecho en este caso ya que no hay más procesamiento asíncrono / diferido. La decisión de "regresar temprano" no es diferente a finalizar cualquier función cuando se completa su trabajo , en lugar de continuar el procesamiento no relacionado o innecesario.

Regresar en el momento apropiado (o de otra manera usar condicionales para evitar ejecutar el "otro" caso) reduce la posibilidad de ejecutar accidentalmente el código en un estado no válido o realizar efectos secundarios no deseados; y como tal hace que el código sea menos propenso a 'romperse inesperadamente'.


1 Esta respuesta técnica también depende del hecho de que, en este caso, el código después del "retorno", si se omite, no producirá un efecto secundario. JavaScript se dividirá felizmente por cero y devolverá + Infinito / -Infinito o NaN.

usuario2864740
fuente
Buena nota al pie !!
HankCa
9

Si no "regresa" después de una resolución / rechazo, pueden suceder cosas malas (como una redirección de página) después de que haya querido que se detuviera. Fuente: me encontré con esto.

Benjamin H
fuente
66
+1 para el ejemplo. Tuve un problema en el que mi programa haría más de 100 consultas de bases de datos no válidas y no pude entender por qué. Resulta que no "regresé" después de un rechazo. Es un pequeño error, pero aprendí mi lección.
AdamInTheOculus
8

La respuesta de Ori ya explica que no es necesario, returnpero es una buena práctica. Tenga en cuenta que el constructor de promesas es seguro, por lo que ignorará las excepciones lanzadas que se pasaron más adelante en el camino, esencialmente tiene efectos secundarios que no puede observar fácilmente.

Tenga en cuenta que el uso returntemprano también es muy común en las devoluciones de llamada:

function divide(nom, denom, cb){
     if(denom === 0){
         cb(Error("Cannot divide by zero");
         return; // unlike with promises, missing the return here is a mistake
     }
     cb(null, nom / denom); // this will divide by zero. Since it's a callback.
} 

Entonces, si bien es una buena práctica en las promesas, se requiere con devoluciones de llamada. Algunas notas sobre su código:

  • Su caso de uso es hipotético, en realidad no use promesas con acciones sincrónicas.
  • El constructor de la promesa ignora los valores de retorno. Algunas bibliotecas le avisarán si devuelve un valor no definido para advertirle sobre el error de regresar allí. La mayoría no son tan inteligentes.
  • El constructor de la promesa es seguro, convertirá las excepciones en rechazos, pero como otros han señalado, una promesa se resuelve una vez.
Benjamin Gruenbaum
fuente
4

En muchos casos, es posible validar los parámetros por separado e inmediatamente devolver una promesa rechazada con Promise.reject (motivo) .

function divide2(numerator, denominator) {
  if (denominator === 0) {
    return Promise.reject("Cannot divide by 0");
  }
  
  return new Promise((resolve, reject) => {
    resolve(numerator / denominator);
  });
}


divide2(4, 0).then((result) => console.log(result), (error) => console.log(error));

Dorad
fuente