Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Skipping promise chain after handling error

Using the https://github.com/kriskowal/q library, I'm wondering if it's possible to do something like this:

// 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()
;

Basically if the service results are bad, I'd like to handle the failure in module A, using logic that is specific to the internals of module A, but still skip the remaining module B functions in the promise chain.

The obvious solution for skipping module B functions is to throw an error/reason from module A. However, then I would need to handle that in module B. And ideally I'd like to do it without needing any extra code in module B for that.

Which may very well be impossible :) Or against some design principles of Q.

In which case, what kind of alternatives would you suggest?

I have two approaches in mind, but both have their disadvantages:

  1. Throw a specific error from module A and add specific handling code to module B:

    .fail(function(reason) {
      if (reason is specificError) {
        performVerySpecificErrorHandling();
      }
      else {
        // Handle the reason in a general way which is ok for module B functions
      }
    })
    
  2. Perform the custom error handling in module A, then after handling the error, throw a fake rejection reason. In module B, add a condition to ignore the fake reason:

    .fail(function(reason) {
      if (reason is fakeReason) {
        // Skip handling
      }
      else {
        // Handle the reason in a general way which is ok for module B functions
      }
    })
    

Solution 1 requires adding module A specific code to module B.

Solution 2 solves this, but the whole fake rejection approach seems very hackish.

Can you recommend other solutions?

like image 317
mjomble Avatar asked Jun 22 '14 21:06

mjomble


People also ask

How do you handle errors in promise chains?

There are two ways in which you can handle errors in your promise chain, either by passing an error handler to then block or using the catch operator.

What happens if you dont resolve promise?

So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.

What can happen when you don't handle errors inside a promise?

The script dies with a message in the console. A similar thing happens with unhandled promise rejections. The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.

What happens when a promise throws an error?

The Promise returned by catch() is rejected if onRejected throws an error or returns a Promise which is itself rejected; otherwise, it is fulfilled.


2 Answers

Let's talk about control constructs.

In JavaScript, code flows in two ways when you call a function.

  • It can return a value to the caller, indicating that it completed successfully.
  • It can throw an error to the caller, indicating that an exceptional operation occurred.

It looks something like:

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");
}

Promises model this exact same behavior.

In Promises, code flows in exactly two ways when you call a function in a .then handler:

  • It can return a promise or a value which indicates that it completed successfully.
  • It can throw an error which indicates that an exceptional state occured.

It looks something like:

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");
});

Promises model flow of control itself

A promise is an abstraction over the notion sequencing operations itself. It describes how control passes from one statement from another. You can consider .then an abstraction over a semicolon.

Let's talk about synchronous code

Let's see how synchronous code would look in your case.

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
    }
}

So, how continuing with the rest of our code is simply returning. This is the same in synchronous code and in asynchronous code with promises. Performing very specific error handling is also ok.

How would we skip the rest of the code in the synchronous version?

doA();
doB();
doC(); // make doD never execute and not throw an exception
doD();

Well, even if not immediately, there is a rather simple way to make doD never execute by causing doC to enter into an infinite loop:

function doC() {
    if (!results.areGood) {
      while(true){} // an infinite loop is the synchronous analogy of not continuing
                    // a promise chain.
    }
}

So, it is possible to never resolve a promise - like the other answer suggests - return a pending promise. However, that is extremely poor flow control since the intent is poorly conveyed to the consumer and it will be likely very hard to debug. Imagine the following API:

moduleA_exportedFunction - this function makes an API request and returns the service as a ServiceData object if the data is available. Otherwise, it enters the program into an endless loop.

A bit confusing, isn't it :)? However, it actually exists in some places. It's not uncommon to find the following in really old APIs.

some_bad_c_api() - this function foos a bar, on failure it terminates the process.

So, what bothers us about terminating the process in that API anyway?

It's all about responsibility.

  • It's the responsibility of the called API to convey whether the API request was successful.
  • It is the responsibility of the caller to decide what to do in each case.

In your case. ModelA is simply breaching the limit of its responsibility, it should not be entitled to make such decisions about the flow of the program. Whoever consumes it should make these decisions.

Throw

The better solution is to throw an error and let the consumer handle it. I'll use Bluebird promises here since they're not only two orders of magnitude faster and have a much more modern API - they also have much much better debugging facilities - in this case - sugar for conditional catches and better stack traces:

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.
});

So in a line - you would do what you would do in synchronous code, as is usually the case with promises.

Note Promise.method is just a convenience function Bluebird has for wrapping functions, I just hate synchronous throwing in promise returning APIs as it creates major breakage.

like image 159
Benjamin Gruenbaum Avatar answered Oct 27 '22 06:10

Benjamin Gruenbaum


It is kind of a design thing. In general, when a module or service returns a promise, you want it to resolve if the call was successful, and to fail otherwise. Having the promise neither resolve or fail, even though you know the call was unsuccessful, is basically a silent failure.

But hey, I don't know the specifics of your modules, or reasons, so if you do want to fail silently in this case, you can do it by returning an unresolved promise:

// 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
      return q.defer().promise;
    }
  });
}
like image 27
Frances McMullin Avatar answered Oct 27 '22 06:10

Frances McMullin