Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

$q: default reject handler

I want to return a $q instance so that if clients don't call 'then' with a reject handler, then a default one runs.

E.g. assume the default is to alert(1)

Then mypromise.then(function(result){...}) will alert 1 but mypromise.then(null, function(reason){alert(2)}) will alert 2

like image 848
IttayD Avatar asked Dec 11 '25 13:12

IttayD


1 Answers

Let's assume there was a way to do that.

So we have a magical machine that can always find out if .then is called with a function second argument for a specific promise or not.

So what?

So you can detect if someone did:

myPromise.then(..., function(){ F(); });

At any point from anywhere at any time. And have G() as a default action.

And...?

You could take a whole program containing lots of code P (1) and convert that code to:

var myPromise = $q.reject();
P; // inline the program's code
myPromise.then(null, function(){}); // attach a handler

Great, so I can do that, so?

Well, now our magical machine can take an arbitrary program P and detect if myPromise had a rejection handler added to it. Now this happens if and only if P does not contain an infinite loop (i.e. it halts). Thus, our method of detecting if a catch handler is ever added is reduced to the halting problem. Which is impossible. (2)

So generally - it is impossible to detect if a .catch handler is ever attached to a promise.

Stop with the mumbu-jumbo, I want a solution!

Good response! Like many problems this one is theoretically impossible but in practice easy enough to solve for practical cases. The key here is a heuristic:

If an error handler is not attached within a microtask (digest in Angular) - no error handlers are ever attached and we can fire the default handler instead.

That is roughly: You never .then(null, function(){}) asynchronously. Promises are resolved asynchronously but the handlers are usually attached synchronously so this works nicely.

// keeping as library agnostic as possible.
var p = myPromiseSource(); // get a promise from source
var then = p.then; // in 1.3+ you can get the constructor and use prototype instead
var t = setTimeout(function(){ // in angular use $timeout, not a microtask but ok
    defaultActionCall(p);// perform the default action!
});
// .catch delegates to `.then` in virtually every library I read so just `then`
p.then = function then(onFulfilled, onRejected){
    // delegate, I omitted progression since no one should use it ever anyway.
    if(typeof onRejected === "function"){ // remove default action
        clearTimeout(t); // `timeout.cancel(t)` in Angular     
    }
    return then.call(this, onFulfilled, onRejected);
};

Is that all?

Well, I just want to add that cases where this extreme approach is needed are rare. When discussing adding rejection tracking to io - several people suggested that if a promise is rejected without a catch then the whole app should likely terminate. So take extra care :)

(1) assume P does not contain a variable myPromise, if it does rename myPromise to something P does not contain.

(2) Of course - one can say that it is enough to read the code of P and not run it in order to detect myPromise gets a rejection handler. Formally we say that we change every return in P and other forms of termination to a return myPromise.then(null, function(){}) instead of simply putting it in the end. this way the "conditionality" is captured.

like image 172
Benjamin Gruenbaum Avatar answered Dec 14 '25 04:12

Benjamin Gruenbaum