¿Por qué no puedo lanzar dentro de un controlador Promise.catch?

127

¿Por qué no puedo simplemente lanzar una Errordevolución de llamada dentro de la captura y dejar que el proceso maneje el error como si estuviera en cualquier otro ámbito?

Si no hago console.log(err)nada, se imprime y no sé nada de lo que sucedió. El proceso acaba de terminar ...

Ejemplo:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

Si las devoluciones de llamada se ejecutan en el hilo principal, ¿por qué Errorse traga un agujero negro?

demian85
fuente
11
No se lo traga un agujero negro. Rechaza la promesa que .catch(…)vuelve.
Bergi
en lugar de .catch((e) => { throw new Error() }), escribir .catch((e) => { return Promise.reject(new Error()) })o simplemente.catch((e) => Promise.reject(new Error()))
chharvey
1
@chharvey todos los fragmentos de código en su comentario tienen un comportamiento exactamente idéntico, excepto que el inicial es obviamente más claro.
Сергей Гринько

Respuestas:

157

Como otros han explicado, el "agujero negro" se debe a que arrojar dentro de una .catchcadena continúa con una promesa rechazada, y no tiene más capturas, lo que lleva a una cadena no terminada, que traga errores (¡malo!)

Agregue una captura más para ver qué sucede:

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

Una captura en el medio de una cadena es útil cuando desea que la cadena continúe a pesar de un paso fallido, pero un relanzamiento es útil para continuar fallando después de hacer cosas como el registro de información o los pasos de limpieza, tal vez incluso alterando qué error es aventado.

Truco

Para hacer que el error aparezca como un error en la consola web, como originalmente pretendías, utilizo este truco:

.catch(function(err) { setTimeout(function() { throw err; }); });

Incluso los números de línea sobreviven, por lo que el enlace en la consola web me lleva directamente al archivo y la línea donde ocurrió el error (original).

Por que funciona

Cualquier excepción en una función llamada como cumplimiento de promesa o controlador de rechazo se convierte automáticamente en un rechazo de la promesa que se supone que debe devolver. El código de promesa que llama a su función se encarga de esto.

Una función llamada por setTimeout, por otro lado, siempre se ejecuta desde el estado estable de JavaScript, es decir, se ejecuta en un nuevo ciclo en el bucle de eventos de JavaScript. Las excepciones no están atrapadas por nada y llegan a la consola web. Dado que errcontiene toda la información sobre el error, incluida la pila original, el archivo y el número de línea, aún se informa correctamente.

foque
fuente
3
Jib, es un truco interesante, ¿puedes ayudarme a entender por qué eso funciona?
Brian Keith
8
Acerca de ese truco: estás lanzando porque quieres iniciar sesión, entonces ¿por qué no simplemente iniciar sesión directamente? Este truco arrojará en un momento "aleatorio" un error inadmisible ... Pero la idea de las excepciones (y la forma en que las promesas tratan con ellas) es hacer que la persona que llama sea responsable de detectar el error y tratarlo. Este código efectivamente hace que sea imposible para la persona que llama lidiar con los errores. ¿Por qué no hacer una función para manejarlo por usted? function logErrors(e){console.error(e)}luego úsalo como do1().then(do2).catch(logErrors). La respuesta en sí es genial por cierto, +1
Stijn de Witt
3
@jib Estoy escribiendo una AWS lambda que contiene muchas promesas conectadas más o menos como en este caso. Para explotar las alarmas y notificaciones de AWS en caso de errores, necesito hacer que el lambda se cuelgue y arroje un error (supongo). ¿Es el truco la única forma de obtener esto?
masciugo
2
@StijndeWitt En mi caso, estaba tratando de enviar detalles de error a mi servidor en el window.onerrorcontrolador de eventos. Solo haciendo el setTimeouttruco se puede hacer esto. De window.onerrorlo contrario , nunca escuchará nada sobre los errores ocurridos en Promise.
hudidit
1
@hudidit Aún así, ya sea console.logo postErrorToServer, simplemente puedes hacer lo que hay que hacer. No hay ninguna razón por la que el código que se encuentra window.onerrorno se pueda descomponer en una función separada y se llame desde 2 lugares. Probablemente sea incluso más corto que la setTimeoutlínea.
Stijn de Witt
46

Cosas importantes para entender aquí

  1. Ambas funciones theny catchdevuelven nuevos objetos de promesa.

  2. Ya sea arrojando o rechazando explícitamente, se moverá la promesa actual al estado rechazado.

  3. Desde theny catchdevolver nuevos objetos de promesa, se pueden encadenar.

  4. Si arroja o rechaza dentro de un controlador de promesa ( theno catch), se manejará en el siguiente controlador de rechazo por el camino de encadenamiento.

  5. Como mencionó jfriend00, los controladores theny catchno se ejecutan sincrónicamente. Cuando un controlador lanza, llegará a su fin de inmediato. Por lo tanto, la pila se desenrollará y se perderá la excepción. Es por eso que lanzar una excepción rechaza la promesa actual.


En su caso, está rechazando dentro do1arrojando un Errorobjeto. Ahora, la promesa actual estará en estado rechazado y el control se transferirá al siguiente controlador, que es thenen nuestro caso.

Como el thencontrolador no tiene un controlador de rechazo, do2no se ejecutará en absoluto. Puede confirmar esto usando console.logdentro de él. Dado que la promesa actual no tiene un controlador de rechazo, también se rechazará con el valor de rechazo de la promesa anterior y el control se transferirá al siguiente controlador que sea catch.

Como catches un controlador de rechazo, cuando lo haces console.log(err.stack);dentro de él, puedes ver el seguimiento de la pila de errores. Ahora, está arrojando un Errorobjeto para que la promesa devuelta catchtambién esté en estado rechazado.

Como no ha adjuntado ningún controlador de rechazo al catch, no puede observar el rechazo.


Puedes dividir la cadena y entender esto mejor, así

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

La salida que obtendrás será algo así como

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

Dentro del catchcontrolador 1, está obteniendo el valor del promiseobjeto como rechazado.

Del mismo modo, la promesa devuelta por el catchcontrolador 1 también se rechaza con el mismo error con el que promisese rechazó y la estamos observando en el segundo catchcontrolador.

thefourtheye
fuente
3
También podría valer la pena agregar que los .then()controladores son asíncronos (la pila se desenrolla antes de que se ejecuten), por lo que las excepciones dentro de ellos deben convertirse en rechazos, de lo contrario no habría controladores de excepciones para atraparlos.
jfriend00
7

Probé el setTimeout()método detallado anteriormente ...

.catch(function(err) { setTimeout(function() { throw err; }); });

Molesto, encontré que esto era completamente inestable. Debido a que arroja un error asincrónico, no puede envolverlo dentro de una try/catchdeclaración, porque catchhabrá dejado de escuchar cuando se produzca el error.

Volví a usar un oyente que funcionó perfectamente y, debido a que es la forma en que se debe usar JavaScript, fue altamente comprobable.

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});
RiggerTheGeek
fuente
3
Jest tiene simulacros de temporizador que deberían manejar esta situación.
jordanbtucker
2

De acuerdo con las especificaciones (ver 3.III.d) :

re. Si llama, lanza una excepción e,
  a. Si se ha llamado a resolvePromise o acceptPromise, ignórelo.
  si. De lo contrario, rechace la promesa con e como la razón.

Eso significa que si arroja una excepción en la thenfunción, se detectará y su promesa será rechazada. catchno tiene sentido aquí, es solo un atajo para.then(null, function() {})

Supongo que desea registrar rechazos no controlados en su código. La mayoría de las bibliotecas promete un fuego unhandledRejectionpara ello. Aquí está la esencia relevante con la discusión al respecto.

just-boris
fuente
Vale la pena mencionar que el unhandledRejectionenlace es para JavaScript del lado del servidor, en el lado del cliente, diferentes navegadores tienen diferentes soluciones. Todavía no lo hemos estandarizado, pero está llegando de manera lenta pero segura.
Benjamin Gruenbaum
1

Sé que esto es un poco tarde, pero me encontré con este hilo, y ninguna de las soluciones fue fácil de implementar para mí, así que se me ocurrió la mía:

Agregué una pequeña función auxiliar que devuelve una promesa, así:

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

Luego, si tengo un lugar específico en cualquiera de mi cadena de promesas donde quiero lanzar un error (y rechazar la promesa), simplemente regreso de la función anterior con mi error construido, así:

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

De esta manera tengo el control de lanzar errores adicionales desde dentro de la cadena de promesa. Si desea manejar también los errores de promesa 'normales', expandiría su captura para tratar los errores 'arrojados' por separado.

Espero que esto ayude, ¡es mi primera respuesta de stackoverflow!

nuudles
fuente
Promise.reject(error)en lugar de new Promise(function (resolve, reject){ reject(error) })(que de todos modos necesitaría una declaración de devolución)
Funkodebat
0

Sí, promete errores de deglución, y solo puede detectarlos .catch, como se explica con más detalle en otras respuestas. Si está en Node.js y desea reproducir el throwcomportamiento normal , imprimiendo el seguimiento de la pila en la consola y el proceso de salida, puede hacerlo

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});
Jesús Carrera
fuente
1
No, eso no es suficiente, ya que necesitaría poner eso al final de cada cadena de promesa que tenga. Más bien enganche en el unhandledRejectionevento
Bergi
Sí, suponiendo que encadene sus promesas para que la salida sea la última función y no se capture después. El evento que mencionas creo que es solo si usas Bluebird.
Jesús Carrera
Bluebird, Q, cuando, las promesas nativas, ... Probablemente se convertirá en un estándar.
Bergi