javascript - sincronas - promesas anidadas
Saltarse cadena de promesa después de manejar el error (3)
Utilizando la biblioteca https://github.com/kriskowal/q , me pregunto si es posible hacer algo como esto:
// Module A
function moduleA_exportedFunction() {
return promiseReturningService().then(function(serviceResults) {
if (serviceResults.areGood) {
// We can continue with the rest of the promise chain
}
else {
performVerySpecificErrorHandling();
// We want to skip the rest of the promise chain
}
});
}
// Module B
moduleA_exportedFunction()
.then(moduleB_function)
.then(moduleB_anotherFunction)
.fail(function(reason) {
// Handle the reason in a general way which is ok for module B functions
})
.done()
;
Básicamente, si los resultados del servicio son malos, me gustaría manejar la falla en el módulo A, usando la lógica que es específica para las partes internas del módulo A, pero aún omito las funciones restantes del módulo B en la cadena de promesas.
La solución obvia para omitir las funciones del módulo B es arrojar un error / razón del módulo A. Sin embargo, entonces tendría que manejar eso en el módulo B. Y, idealmente, me gustaría hacerlo sin necesitar ningún código adicional en el módulo B para ese.
Que bien puede ser imposible :) O en contra de algunos principios de diseño de Q.
En ese caso, ¿qué tipo de alternativas sugerirías?
Tengo dos enfoques en mente, pero ambos tienen sus desventajas:
Tire un error específico del módulo A y agregue un código de manejo específico al módulo B:
.fail(function(reason) { if (reason is specificError) { performVerySpecificErrorHandling(); } else { // Handle the reason in a general way which is ok for module B functions } })
Realice el manejo de error personalizado en el módulo A, luego, después de manejar el error, genere un motivo de rechazo falso. En el módulo B, agregue una condición para ignorar el motivo falso:
.fail(function(reason) { if (reason is fakeReason) { // Skip handling } else { // Handle the reason in a general way which is ok for module B functions } })
La solución 1 requiere agregar el código específico del módulo A al módulo B.
La solución 2 resuelve esto, pero todo el enfoque de rechazo falso parece muy hackish.
¿Puedes recomendar otras soluciones?
Hablemos de construcciones de control.
En JavaScript, el código fluye de dos maneras cuando llama a una función.
- Puede
return
un valor a la persona que llama, lo que indica que se completó correctamente. - Puede
throw
un error a la persona que llama, lo que indica que se produjo una operación excepcional.
Se ve algo así como:
function doSomething(){ // every function ever
if(somethingBad) throw new Error("Error operating");
return value; // successful completion.
}
try{
doSomething();
console.log("Success");
} catch (e){
console.log("Boo");
}
Las promesas modelan este mismo comportamiento.
En Promesas, el código fluye exactamente de dos maneras cuando se llama a una función en un controlador .then
:
- Puede
return
una promesa o un valor que indique que se completó correctamente. - Puede
throw
un error que indica que se produjo un estado excepcional.
Se ve algo así como:
var doSomething = Promise.method(function(){
if(somethingBad) throw new Error("Error operating");
return someEventualValue(); // a direct value works here too
}); // See note, in Q you''d return Q.reject()
Promise.try(function(){ // in Q that''s Q().then
doSomething();
console.log("Success");
}).catch(function(e){
console.log("Boo");
});
Promete el flujo de control del modelo en sí mismo
Una promesa es una abstracción sobre las operaciones de secuenciación de la noción misma. Describe cómo el control pasa de una declaración a otra. Puedes considerar. .then
una abstracción sobre un punto y coma.
Hablemos de código síncrono
Veamos cómo se vería el código sincrónico en tu caso.
function moduleA_exportedFunction() {
var serviceResults = someSynchronousFunction();
if (serviceResults.areGood) {
// We can continue with the rest of our code
}
else {
performVerySpecificErrorHandling();
// We want to skip the rest of the chain
}
}
Entonces, ¿cómo continuar con el resto de nuestro código simplemente está returning
? Esto es lo mismo en código síncrono y en código asíncrono con promesas. Realizar un manejo de errores muy específico también está bien.
¿Cómo omitiríamos el resto del código en la versión síncrona?
doA();
doB();
doC(); // make doD never execute and not throw an exception
doD();
Bueno, incluso si no de inmediato, hay una forma bastante simple de hacer que doD nunca se ejecute haciendo que doC ingrese en un bucle infinito:
function doC() {
if (!results.areGood) {
while(true){} // an infinite loop is the synchronous analogy of not continuing
// a promise chain.
}
}
Por lo tanto, es posible nunca resolver una promesa, como sugiere la otra respuesta, devolver una promesa pendiente. Sin embargo, ese es un control de flujo extremadamente pobre ya que la intención es mal transmitida al consumidor y probablemente será muy difícil de depurar. Imagine la siguiente API:
moduleA_exportedFunction : esta función realiza una solicitud API y devuelve el servicio como un objeto
ServiceData
si los datos están disponibles. De lo contrario, ingresa al programa en un bucle sin fin .
Un poco confuso, ¿no es así? Sin embargo, en realidad existe en algunos lugares. No es raro encontrar lo siguiente en API realmente antiguas.
some_bad_c_api () - esta función foos a bar, en caso de falla finaliza el proceso .
Entonces, ¿qué nos molesta de terminar el proceso en esa API de todos modos?
Se trata de responsabilidad.
- Es responsabilidad de la llamada API comunicar si la solicitud de la API fue exitosa.
- Es la responsabilidad de quien llama decidir qué hacer en cada caso.
En tu caso. ModelA simplemente está incumpliendo el límite de su responsabilidad, no debería tener derecho a tomar tales decisiones sobre el flujo del programa. Quien lo consuma debe tomar estas decisiones.
Lanzar
La mejor solución es arrojar un error y dejar que el consumidor lo maneje. Usaré las promesas de Bluebird aquí ya que no solo son dos órdenes de magnitud más rápidas y tienen una API mucho más moderna, también tienen mucho mejores instalaciones de depuración, en este caso, azúcar para capturas condicionales y mejores trazas de pila:
moduleA_exportedFunction().then(function(result){
// this will only be reached if no error occured
return someOtherApiCall();
}).then(function(result2){
// this will be called if the above function returned a value that is not a
// rejected promise, you can keep processing here
}).catch(ApiError,function(e){
// an error that is instanceof ApiError will reach here, you can handler all API
// errors from the above `then`s in here. Subclass errors
}).catch(NetworkError,function(e){
// here, let''s handle network errors and not `ApiError`s, since we want to handle
// those differently
}).then(function(){
// here we recovered, code that went into an ApiError or NetworkError (assuming
// those catch handlers did not throw) will reach this point.
// Other errors will _still_ not run, we recovered successfully
}).then(function(){
throw new Error(); // unless we explicitly add a `.catch` with no type or with
// an `Error` type, no code in this chain will run anyway.
});
Así que en una línea: harías lo que harías en código síncrono, como suele ser el caso con las promesas.
Nota: Promise.method es solo una función de conveniencia que Bluebird tiene para envolver funciones. Odio el lanzamiento sincrónico de las promesas de API que regresan, ya que crea una rotura importante.
Es una especie de cosa de diseño. En general, cuando un módulo o servicio devuelve una promesa, usted desea que se resuelva si la llamada fue exitosa y, de lo contrario, fallar. Tener la promesa de no resolver o fallar, aunque sepa que la llamada no tuvo éxito, es básicamente una falla silenciosa.
Pero bueno, no sé los detalles de sus módulos o razones, así que si quiere fallar silenciosamente en este caso, puede hacerlo devolviendo una promesa no resuelta:
// Módulo A
function moduleA_exportedFunction() {
return promiseReturningService().then(function(serviceResults) {
if (serviceResults.areGood) {
// We can continue with the rest of the promise chain
}
else {
performVerySpecificErrorHandling();
// We want to skip the rest of the promise chain
return q.defer().promise;
}
});
}
Inspirado por los comentarios y la respuesta de Benjamin Gruenbaum: si escribiera esto en código síncrono, haría que moduleA_exportedFunction
devuelva a shouldContinue
boolean.
Entonces, con promesas, básicamente sería algo como esto (descargo de responsabilidad: esto es psuedo-code-ish y no probado)
// Module A
function moduleA_exportedFunction() {
return promiseReturningService().then(function(serviceResults) {
if (serviceResults.areGood) {
// We can continue with the rest of the promise chain
return true;
}
else {
performVerySpecificErrorHandling();
// We want to skip the rest of the promise chain
return false;
}
});
}
// Module B
moduleA_exportedFunction()
.then(function(shouldContinue) {
if (shouldContinue) {
return moduleB_promiseReturningFunction().then(moduleB_anotherFunction);
}
})
.fail(function(reason) {
// Handle the reason in a general way which is ok for module B functions
// (And anything unhandled from module A would still get caught here)
})
.done()
;
Requiere un código de manejo en el módulo B, pero la lógica no es específica para las partes internas del módulo A ni implica tirar e ignorar errores falsos: ¡misión cumplida! :)