1

Not sure if I'm clear enough with this title, but assume that I have a class called Foo with method1, method2 and method3. I promisify its methods with promisifyAll.

Then I have a then-chain and I want to cancel the operation in the middle of the second or first then, and no further then should be called.

I read about Cancellation (http://bluebirdjs.com/docs/api/cancellation.html) but I don't know how to implement it with promisifyAll.

The code I got plus what I need:

var bluebird = require('bluebird');

function Foo() { }

Foo.prototype.method1 = function (cb) {};

Foo.prototype.method2 = function (cb) {};

Foo.prototype.method3 = function (cb) {};

var foo = bluebird.promisifyAll(new Foo());

foo.method1Async()
.then(function (r1) {
  // cancel then-chain
  res.json("Task stopped");
  // just stop right here
  promises.cancel();
})
.then(function (r2) {
  console.log(r2);
}).then(function (r3) {
  console.log(r3);
})
.catch(function (er) {
  console.log('Catch!');
});

What is the proper way of having this result? I know I can just throw something and catch it in the catch method, but this would make a very big change in my real code.

Victor Ferreira
  • 6,151
  • 13
  • 64
  • 120
  • If you know that you want to cancel them, why do you put them there in the first place? Is the cancellation conditional? Is it really done from within the `then` callback? Then you should just throw an exception; cancellation is for external signaling. – Bergi Nov 09 '16 at 23:37
  • "*I know I can just throw something and catch it in the catch method, but this would make a very big change in my real code.*" - why? It's the right thing to do. What is your real code, and what's the problem with throwing in there? – Bergi Nov 09 '16 at 23:43
  • @Bergi You're right, in this case the `then` callback should either throw an exception or return e rejected promise which has the same effect. I tried to explain it in detail in [my answer below](http://stackoverflow.com/questions/40515775/use-cancel-inside-a-then-chain-created-by-promisifyall/40515981#40515981) but the OP didn't answer if it worked for him. Please comment if my answer needs any improvement or clarification. Thanks. – rsp Nov 10 '16 at 09:02
  • the cancellation is conditional. I didn't want to throw/catch because I was using throw/catch just to deal with exceptions, and not alternative flows. – Victor Ferreira Nov 10 '16 at 10:25
  • If you want to use flow control, [do it with an `if` statement](http://stackoverflow.com/a/29500221/1048572). But Bluebird's tested exceptions make throwing a simple alternative. – Bergi Nov 10 '16 at 19:37

1 Answers1

1

Try something like this:

var bluebird = require('bluebird');

function Foo() { }

Foo.prototype.method1 = function (cb) { cb(null, 'method1'); };
Foo.prototype.method2 = function (cb) { cb(null, 'method2'); };
Foo.prototype.method3 = function (cb) { cb(null, 'method3'); };

var foo = bluebird.promisifyAll(new Foo());

foo.method1Async()
.then(function (r1) {
  console.log('step 1');
  // cancel then-chain
  console.log("Task stopped");
  // just stop right here
  return bluebird.reject('some reason');
})
.then(function (r2) {
  console.log('step 2');
  console.log(r2);
}).then(function (r3) {
  console.log('step 3');
  console.log(r3);
})
.catch(function (er) {
  console.log('Catch!');
  console.log('Error:', er);
});

Instead of:

  return bluebird.reject('some reason');

you can use:

  throw 'some reason';

and the result would be the same but you didn't want to throw errors so you can return a rejected promise instead.

Update 1

But if your intention is to run all 3 methods in series then you will also need to return the next promise at each step with something like this:

var bluebird = require('bluebird');

function Foo() { }

Foo.prototype.method1 = function (cb) { cb(null, 'method1'); };
Foo.prototype.method2 = function (cb) { cb(null, 'method2'); };
Foo.prototype.method3 = function (cb) { cb(null, 'method3'); };

var foo = bluebird.promisifyAll(new Foo());

foo.method1Async()
.then(function (r1) {
  console.log('step 1');
  console.log('got value:', r1);
  // cancel? change to true:
  var cancel = false;
  if (cancel) {
    console.log("Task stopped");
    return bluebird.reject('some reason');
  } else {
    console.log('Keep going');
    return foo.method2Async();
  }
})
.then(function (r2) {
  console.log('step 2');
  console.log('got value:', r2);
  return foo.method3Async();
}).then(function (r3) {
  console.log('step 3');
  console.log('got value:', r3);
})
.catch(function (er) {
  console.log('Catch!');
  console.log('Error:', er);
});

Currently the code in your question would never run any other method than the first one.

Update 2

Another example that doesn't call the last catch for that case:

foo.method1Async()
.then(function (r1) {
  console.log('step 1');
  console.log('got value:', r1);
  // cancel? change to true:
  var cancel = true;
  if (cancel) {
    console.log("Task stopped");
    return bluebird.reject('some reason');
  } else {
    console.log('Keep going');
    return foo.method2Async();
  }
})
.then(function (r2) {
  console.log('step 2');
  console.log('got value:', r2);
  return foo.method3Async();
}).then(function (r3) {
  console.log('step 3');
  console.log('got value:', r3);
})
.catch(function (er) {
  if (er === 'some reason') {
    return bluebird.resolve('ok');
  } else {
    return bluebird.reject(er);
  }
})
.catch(function (er) {
  console.log('Catch!');
  console.log('Error:', er);
});

Explanation

Think of it this way: in synchronous code if you had:

r1 = fun1();
r2 = fun2();
r3 = fun3();

then the only way for fun1 to cancel the execution of fun2 and fun3 would be to throw an exception. And similarly for promises, the only way that one then handler could cancel the execution of the next then handler is to return a rejected promise. And just like with synchronous code that thrown exception would be caught by the catch block, here that rejected promise would be passed to the catch handler.

With synchronous code you can have an internal try/catch that catches the exception and rethrows it if it isn't the specific one that you used to cancel your execution. With promises you can have an earlier catch handler that does essentially the same.

This is what happens with the code in Update 2. The rejection reason is compared to some value (which is 'some reason' in that example) and if it is equal then a resolved promise is returned and so the next catch handler is not called. If it is not equal then the rejection reason is returned again as a rejected promise witch is then passed to the next catch handler as a "real" error that you want that last catch handler to handle.

rsp
  • 107,747
  • 29
  • 201
  • 177
  • But `reject()` will be caught by the 'catch' statement, right? isn't there a way to make it stop, but not necessarily get caught? my `catch's` already have a logic and they are dealing with real errors. in some cases I want to cancel, but not mix things up – Victor Ferreira Nov 09 '16 at 21:11
  • @VictorFerreira Yes but you can add another `catch` that stops that `catch` from running. See Update 2 in my answer. At every `then` step you can only return another promise - that can either get resolved or rejected. So if you don't want the next `then` to run then you have no other option than return a rejected promise or throw an error. Either way it will call the next `catch` but that `catch` can see what was the value that it got and either return a resolved promise or rejected promise. It's like rethrowing the exception for certain kinds of exceptions but not others in synchronous code. – rsp Nov 09 '16 at 21:21
  • @VictorFerreira See the Explanation part in my answer that I added. I hope it clarifies the issue. – rsp Nov 09 '16 at 21:34
  • I liked your idea of returning a rejected promise for the next catch. so I created a class called `PromiseHandler` and a method `cancel()`. I throw it and check in the middleware-catch. if it was not a PromiseHandler canceled, I return a rejected promise that will be caught by the next `catch` – Victor Ferreira Nov 10 '16 at 15:59