Promesas de JavaScript: rechazar frente a tirar

385

He leído varios artículos sobre este tema, pero todavía no me queda claro si hay una diferencia entre Promise.reject vs. lanzar un error. Por ejemplo,

Usando Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

Usando tiro

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

Prefiero usar throwsimplemente porque es más corto, pero me preguntaba si hay alguna ventaja de uno sobre el otro.

Naresh
fuente
99
Ambos métodos producen exactamente la misma respuesta. El .then()controlador detecta la excepción lanzada y la convierte automáticamente en una promesa rechazada. Como he leído que las excepciones lanzadas no son particularmente rápidas de ejecutar, supongo que devolver la promesa rechazada podría ser un poco más rápido de ejecutar, pero tendrías que idear una prueba en varios navegadores modernos si eso fuera importante. Yo personalmente uso throwporque me gusta la legibilidad.
jfriend00
@webduvet no con Promesas: están diseñadas para funcionar con tiros.
Joe
15
Una desventaja throwes que no daría lugar a una promesa rechazada si se lanzara desde una devolución de llamada asincrónica, como un setTimeout. jsfiddle.net/m07van33 @Blondie su respuesta fue correcta.
Kevin B
@joews no significa que sea bueno;)
webduvet
1
Ah cierto. Entonces, una aclaración a mi comentario sería, "si fue arrojado desde una devolución de llamada asincrónica que no fue prometida " . Sabía que había una excepción a eso, simplemente no podía recordar qué era. También prefiero usar throw simplemente porque me parece más legible y me permite omitirlo rejectde mi lista de parámetros.
Kevin B

Respuestas:

346

No hay ninguna ventaja de usar uno frente al otro, pero hay un caso específico en el throwque no funcionará. Sin embargo, esos casos se pueden arreglar.

Cada vez que esté dentro de una devolución de llamada de promesa, puede usarla throw. Sin embargo, si está en cualquier otra devolución de llamada asincrónica, debe usarla reject.

Por ejemplo, esto no activará la captura:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

En cambio, te queda una promesa no resuelta y una excepción no alcanzada. Ese es un caso en el que desearía utilizarlo reject. Sin embargo, puede solucionar esto de dos maneras.

  1. mediante el uso de la función de rechazo de Promise original dentro del tiempo de espera:

new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works!
});

  1. al prometer el tiempo de espera:

function timeout(duration) { // Thanks joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'worky!';
  // return Promise.reject('worky'); also works
}).catch(function(e) {
  console.log(e); // 'worky!'
});

Kevin B
fuente
54
Vale la pena mencionar que los lugares dentro de una devolución de llamada asincrónica no prometida que no puede usar throw error, tampoco puede usar, return Promise.reject(err)que es lo que el OP nos pidió que comparemos. Esta es básicamente la razón por la que no debe poner devoluciones de llamada asíncronas dentro de las promesas. Promete todo lo que es asíncrono y luego no tienes estas restricciones.
jfriend00
99
"Sin embargo, si está en cualquier otro tipo de devolución de llamada" realmente debería ser "Sin embargo, si está en cualquier otro tipo de devolución de llamada asincrónica ". Las devoluciones de llamada pueden ser sincrónicas (p. Ej., Con Array#forEach) y con ellas, lanzarlas dentro de ellas funcionaría.
Félix Saparelli
2
@KevinB leyendo estas líneas "hay un caso específico en el que tirar no funcionará". y "Cada vez que esté dentro de una devolución de llamada prometida, puede usar throw. Sin embargo, si está en cualquier otra devolución de llamada asincrónica, debe usar el rechazo". Tengo la sensación de que los fragmentos de ejemplo mostrarán casos en los throwque no funcionarán y, en cambio, Promise.rejectes una mejor opción. Sin embargo, los fragmentos no se ven afectados con ninguna de esas dos opciones y dan el mismo resultado independientemente de lo que elija. ¿Me estoy perdiendo de algo?
Anshul
2
si. si usa throw en un setTimeout, no se llamará a catch. debe usar el rejectque se pasó a la new Promise(fn)devolución de llamada.
Kevin B
2
@KevinB gracias por quedarte junto. El ejemplo dado por OP menciona que específicamente quería comparar return Promise.reject()y throw. No menciona la rejectdevolución de llamada dada en la new Promise(function(resolve, reject))construcción. Entonces, si bien sus dos fragmentos demuestran correctamente cuándo debe usar la devolución de llamada de resolución, la pregunta de OP no fue esa.
Anshul
202

Otro hecho importante es que reject() NO termina el flujo de control como lo hace una returndeclaración. Por el contrario throw, termina el flujo de control.

Ejemplo:

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

vs

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

lukyer
fuente
51
Bueno, el punto es correcto, pero la comparación es complicada. Porque normalmente debe devolver su promesa rechazada por escrito return reject(), para que la siguiente línea no se ejecute.
AZ.
77
¿Por qué querrías devolverlo?
lukyer
31
En este caso, return reject()es simplemente una abreviatura de, por reject(); returnejemplo, lo que desea es terminar el flujo. El valor de retorno del ejecutor (la función pasada a new Promise) no se utiliza, por lo que es seguro.
Félix Saparelli
47

Sí, la mayor diferencia es que el rechazo es una función de devolución de llamada que se lleva a cabo después de que se rechaza la promesa, mientras que throw no se puede usar de forma asincrónica. Si opta por usar Rechazar, el código seguirá funcionando normalmente de forma asíncrona, mientras que tiro priorizará completar la función de resolución (esta función se ejecutará inmediatamente).

Un ejemplo que he visto que me ayudó a aclarar el problema fue que podía establecer una función de tiempo de espera con rechazo, por ejemplo:

new Promise(_, reject) {
 setTimeout(reject, 3000);
});

Lo anterior no podría ser posible escribir con tiro.

En su pequeño ejemplo, la diferencia es indistinguible, pero cuando se trata de un concepto asincrónico más complicado, la diferencia entre los dos puede ser drástica.

Rubio
fuente
1
Esto suena como un concepto clave, pero no lo entiendo como está escrito. Todavía demasiado nuevo para Promises, supongo.
David Spector
43

TLDR: una función es difícil de usar cuando a veces devuelve una promesa y a veces arroja una excepción. Al escribir una función asíncrona, prefiera señalar la falla devolviendo una promesa rechazada

Su ejemplo particular ofusca algunas distinciones importantes entre ellos:

Debido a que está manejando errores dentro de una cadena de promesa, las excepciones lanzadas se convierten automáticamente en promesas rechazadas. Esto puede explicar por qué parecen ser intercambiables, no lo son.

Considere la siguiente situación:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if ( idToken ) {
      return fetch(`https://someValidateEndpoint`, {
        headers: {
          Authorization: `Bearer ${idToken}`
        }
      })
    } else {
      throw new Error('No Token Found In Local Storage')
    }
  }

Esto sería un antipatrón porque necesitaría admitir casos de error de sincronización y asíncrono. Podría parecerse a algo como:

try {
  function onFulfilled() { ... do the rest of your logic }
  function onRejected() { // handle async failure - like network timeout }
  checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
  // Error('No Token Found In Local Storage')
  // handle synchronous failure
} 

No es bueno y aquí es exactamente donde Promise.reject(disponible en el ámbito global) viene al rescate y efectivamente se diferencia de sí mismo throw. El refactor ahora se convierte en:

checkCredentials = () => {
  let idToken = localStorage.getItem('some_token');
  if (!idToken) {
    return Promise.reject('No Token Found In Local Storage')
  }
  return fetch(`https://someValidateEndpoint`, {
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  })
}

Esto ahora le permite usar solo uno catch()para fallas en la red y la verificación de errores síncronos por falta de tokens:

checkCredentials()
      .catch((error) => if ( error == 'No Token' ) {
      // do no token modal
      } else if ( error === 400 ) {
      // do not authorized modal. etc.
      }
Maxwell
fuente
1
Sin embargo, el ejemplo de Op siempre devuelve una promesa. La pregunta se refiere a si debe usar Promise.rejecto throwcuándo desea devolver una promesa rechazada (una promesa que saltará a la siguiente .catch()).
Marcos Pereira
@maxwell - Me gustas tu ejemplo. Al mismo tiempo, si en la búsqueda agregará una captura y en ella lanzará la excepción, entonces será seguro usar intentar ... capturar ... No hay un mundo perfecto en el flujo de excepción, pero creo que usar uno un solo patrón tiene sentido, y combinar los patrones no es seguro (alineado con su patrón frente a la analogía antipatrón).
user3053247
1
Excelente respuesta, pero aquí encuentro una falla: este patrón asume que todos los errores se manejan devolviendo un Promise.reject. ¿Qué sucede con todos los errores inesperados que simplemente podrían arrojarse desde checkCredentials ()?
chenop
1
Sí, tienes razón @chenop: para detectar esos errores inesperados que necesitarías envolver en try / catch still
maxwell el
No entiendo el caso de @ maxwell. ¿No podría simplemente estructurarlo para que lo haga checkCredentials(x).then(onFulfilled).catch(e) {}y tener el catchcontrol tanto del caso de rechazo como del caso de error arrojado?
Ben Wheeler
5

Un ejemplo para probar. Simplemente cambie isVersionThrow a false para usar el rechazo en lugar del lanzamiento.

const isVersionThrow = true

class TestClass {
  async testFunction () {
    if (isVersionThrow) {
      console.log('Throw version')
      throw new Error('Fail!')
    } else {
      console.log('Reject version')
      return new Promise((resolve, reject) => {
        reject(new Error('Fail!'))
      })
    }
  }
}

const test = async () => {
  const test = new TestClass()
  try {
    var response = await test.testFunction()
    return response 
  } catch (error) {
    console.log('ERROR RETURNED')
    throw error 
  }  
}

test()
.then(result => {
  console.log('result: ' + result)
})
.catch(error => {
  console.log('error: ' + error)
})

Chris Livdahl
fuente