Get state of Angular deferred?

42,593

Solution 1

Update:

Due to refactoring of $q this is now possible although not documented:

promise.$$state.status === 0 // pending
promise.$$state.status === 1 // resolved
promise.$$state.status === 2 // rejected

Original:

Unlike most promise libraries (Bluebird,Q, when, RSVP etc), $q does not expose a synchronous inspection API.

There is no way to achieve this from the outside.

You have to call .then on the promise and code in that handler will run when the promise fulfills.

Solution 2

The answer to your question is: yes, there is a way. The other answers nicely cover the built-in limitations of $q. However, it's easy to add a state property to $q using the $provide service's decorator function.

  $provide.decorator('$q', function ($delegate) {
    var defer = $delegate.defer;
    $delegate.defer = function() {
      var deferred = defer();

      deferred.promise.state = deferred.state = 'pending';

      deferred.promise.then(function() {
        deferred.promise.state = deferred.state = 'fulfilled';
      }, function () {
        deferred.promise.state = deferred.state = 'rejected';
      }); 

      return deferred;
    };
    return $delegate;
  });

Put this decorator inside of a config block, and all $q-instantiated deferred and promise objects will have a state property with the value pending, fulfilled, or rejected.

Check out this plunk


Skeptical?

you are effectively modifying $q itself, wrapping every deferred with another deferred

Actually this is not the case. $q's original defer() constructor is called exactly one time. It is simply decorated with additional functionality by internally attaching an event handler via then. [Note that an additional defer object is instantiated as a result of the additional then callback which is automatically created with each deferred object... which is to be expected because this is how angular works internally.]

this wouldn't work because promises shouldn't be created with deferred but chained from promises that are returned from apis

Note that this code will decorate every deferred (and thus promise object) which is created by the $q service. This means that any API which utilizes $q will be automatically decorated with the state property. So regardless of how you use $q, whether with some API or on it's own, this solution decorates both the deferred object and the promise, and I have provided the plunk to prove it.


Production-worthy?

This approach is unit testable, it's guaranteed not to break any application already using $q, and it's flexible in the sense that you could later add additional decorators to $q without modifying the old one(s).

Solution 3

Updated:

Unfortunately this doesn't looks like its possible with $q. You'll have to put this code inside your then method.

myPromise()
.then(function() {
    // everything in here resolved
},
function() {
    // everything in here rejected
},
function() {
    // everything in here pending (with progress back)
});

Other:

This is for the Q library not angular's $q but similar.

Angular is inspired by the Q library, check out the source, its actually not that scary. https://github.com/kriskowal/q/blob/v1/q.js

You can use myPromise.inspect().state there are ['pending', 'rejected', 'fulfilled']

You also have:

myPromise.isFulfilled();
myPromise.isPending();
myPromise.isRejected();

Check out this JSfiddle and open the console for logged results. http://jsfiddle.net/S6LzP/

More granular, Looking at the defer function on line 488:

function defer() {
    // if "messages" is an "Array", that indicates that the promise has not yet
    // been resolved.  If it is "undefined", it has been resolved.  Each
    // element of the messages array is itself an array of complete arguments to
    // forward to the resolved promise.  We coerce the resolution value to a
    // promise using the `resolve` function because it handles both fully
    // non-thenable values and other thenables gracefully.
    var messages = [], progressListeners = [], resolvedPromise;

    var deferred = object_create(defer.prototype);
    var promise = object_create(Promise.prototype);

    promise.promiseDispatch = function (resolve, op, operands) {
        var args = array_slice(arguments);
        if (messages) {
            messages.push(args);
            if (op === "when" && operands[1]) { // progress operand
                progressListeners.push(operands[1]);
            }
        } else {
            nextTick(function () {
                resolvedPromise.promiseDispatch.apply(resolvedPromise, args);
            });
        }
    };

    // XXX deprecated
    promise.valueOf = function () {
        if (messages) {
            return promise;
        }
        var nearerValue = nearer(resolvedPromise);
        if (isPromise(nearerValue)) {
            resolvedPromise = nearerValue; // shorten chain
        }
        return nearerValue;
    };

    promise.inspect = function () {
        if (!resolvedPromise) {
            return { state: "pending" };
        }
        return resolvedPromise.inspect();
    };

    if (Q.longStackSupport && hasStacks) {
        try {
            throw new Error();
        } catch (e) {
            // NOTE: don't try to use `Error.captureStackTrace` or transfer the
            // accessor around; that causes memory leaks as per GH-111. Just
            // reify the stack trace as a string ASAP.
            //
            // At the same time, cut off the first line; it's always just
            // "[object Promise]\n", as per the `toString`.
            promise.stack = e.stack.substring(e.stack.indexOf("\n") + 1);
        }
    }

    // NOTE: we do the checks for `resolvedPromise` in each method, instead of
    // consolidating them into `become`, since otherwise we'd create new
    // promises with the lines `become(whatever(value))`. See e.g. GH-252.

    function become(newPromise) {
        resolvedPromise = newPromise;
        promise.source = newPromise;

        array_reduce(messages, function (undefined, message) {
            nextTick(function () {
                newPromise.promiseDispatch.apply(newPromise, message);
            });
        }, void 0);

        messages = void 0;
        progressListeners = void 0;
    }

    deferred.promise = promise;
    deferred.resolve = function (value) {
        if (resolvedPromise) {
            return;
        }

        become(Q(value));
    };

    deferred.fulfill = function (value) {
        if (resolvedPromise) {
            return;
        }

        become(fulfill(value));
    };
    deferred.reject = function (reason) {
        if (resolvedPromise) {
            return;
        }

        become(reject(reason));
    };
    deferred.notify = function (progress) {
        if (resolvedPromise) {
            return;
        }

        array_reduce(progressListeners, function (undefined, progressListener) {
            nextTick(function () {
                progressListener(progress);
            });
        }, void 0);
    };

    return deferred;
}

Mostly notably the method at the very bottom deferred.notify.

Example usage:

function requestOkText(url) {
    var request = new XMLHttpRequest();
    var deferred = Q.defer();

    request.open("GET", url, true);
    request.onload = onload;
    request.onerror = onerror;
    request.onprogress = onprogress;
    request.send();

    function onload() {
        if (request.status === 200) {
            deferred.resolve(request.responseText);
        } else {
            deferred.reject(new Error("Status code was " + request.status));
        }
    }

    function onerror() {
        deferred.reject(new Error("Can't XHR " + JSON.stringify(url)));
    }

    function onprogress(event) {
        deferred.notify(event.loaded / event.total);
    }

    return deferred.promise;
}

requestOkText("http://localhost:3000")
.then(function (responseText) {
    // If the HTTP response returns 200 OK, log the response text.
    console.log(responseText);
}, function (error) {
    // If there's an error or a non-200 status code, log the error.
    console.error(error);
}, function (progress) {
    // Log the progress as it comes in.
    console.log("Request progress: " + Math.round(progress * 100) + "%");
});
Share:
42,593
Evan Hobbs
Author by

Evan Hobbs

Updated on July 09, 2022

Comments

  • Evan Hobbs
    Evan Hobbs almost 2 years

    With jQuery deferreds I'm used to be able to check the current state like this:

    var defer = $.Deferred();
    defer.state();  //Returns the state of the deferred, eg 'resolved'
    

    Is there a way to do the same for Angular deferreds? (or even better promises)

  • Benjamin Gruenbaum
    Benjamin Gruenbaum about 10 years
    On the bright side you can use a stronger Promise implementation in Angular seamlessly
  • Evan Hobbs
    Evan Hobbs about 10 years
    I think Angular's deferred's are inspired by the Q library but they're missing a lot of the functionality of Q: docs.angularjs.org/api/ng/service/$q... From what I've seen those methods aren't available on Angular promises
  • Travis
    Travis about 10 years
    My mistake, I've quickly looked through the angular github github.com/angular/angular.js/blob/master/src/ng/q.js and it looks like this isn't possible but you should still be able to use notify. :/
  • Esailija
    Esailija about 10 years
  • Esailija
    Esailija about 10 years
    Yes this wouldn't work because promises shouldn't be created with deferred but chained from promises that are returned from apis
  • Gil Birman
    Gil Birman about 10 years
    I've responded to your comments in the answer.
  • Benjamin Gruenbaum
    Benjamin Gruenbaum about 10 years
    @GilBirman I know what a provider is :) You are creating an extra deferred for every deferred (fourth line). That said, I give you points for creativity (although I hope you don't actually do this in Angular production code ;)).
  • Gil Birman
    Gil Birman about 10 years
    @BenjaminGruenbaum, I've updated the answer to explain why an additional defer object is being created, but not (as you thought) because of line 4. I don't use this code in production code, but it seems alright to me.
  • Nate Barbettini
    Nate Barbettini over 9 years
    @BenjaminGruenbaum This may be a dumb question, but I'm curious as to why specifically you would be uncomfortable using this in production code.
  • Benjamin Gruenbaum
    Benjamin Gruenbaum over 9 years
    @NateBarbettini well - for one thing $q went through several big changes over the lifespan of 1.2, a huge internal overhaul between 1.2 and 1.3, and almost underwent an even bigger overhaul. Today you'd do this completely differently (deferred and promises are prototype based) - you could override .then directly without the overhead of this solution (creating an overhead and allocating an extra closure for every single promise through Angular) Promises are slow in Angular as it is (getting better recently).
  • Gil Birman
    Gil Birman over 9 years
    I'm curious to hear the use-case of promises where instantiating a closure and some simple objects per promise would create any perceptible performance difference.
  • edamon
    edamon about 9 years
    @BenjaminGruenbaum what version of Angular is this in? 1.3.5? 1.2.28? thanks
  • edamon
    edamon about 9 years
    It looks like it was added to 1.3.0 - github.com/angular/angular.js/blob/v1.3.0/src/ng/q.js. I couldn't find any reference to $$state before that.
  • muttonUp
    muttonUp almost 9 years
    status can also be -1, but I'm not sure what it signifies? github.com/angular/angular.js/blob/v1.4.3/src/ng/q.js#L365
  • Rockallite
    Rockallite over 8 years
    Properties start with "$$" should be considered as private API, and not used in your own code. However, there's not public API for exposing status of an Angular promise, I think.
  • Petr Peller
    Petr Peller over 8 years
    This definitely feels cleaner/safer than the solution in the accepted answer.
  • Tongfa
    Tongfa almost 8 years
    @muttonUp: -1 appears to be a second resolved case, like 1. It is used to signify when the resolved value is a function, or if the value needs to be passed down a promise chain. I imagine -1 means something like "so far so good, but we have more code to run before the resolution is complete".
  • p0lar_bear
    p0lar_bear almost 7 years
    This isn't working consistently in Angular 1.6.4. This only decorates promises made via $q.defer, and not every promise is generated from that method. In specific, the reject, when/resolve, and all methods specifically call new Promise() internally whereas race calls defer. The best way to handle this would be to decorate the constructor for Promise, but that entire API is internal.