29

I have a function that takes a single argument. I need to be able to tell if this argument is a jQuery Promise or Deferred object. If not, then the value may be of any type and have any properties, so it's not safe to just just for the presence of the promise methods.

Here's an example of how I'd like my function to behave:

function displayMessage(message) {
  if (message is a Promise or Deferred) {
    message.then(displayMessage);
  } else {
    alert(message);
  }
}

Notice the recursive handling of promises: if a promise is resolved with another promise value we don't display it, we wait for it to be resolved. If it returns yet another promise, repeat.


This is important because if this were not the case, I would just be able to use jQuery.when:

function displayMessage(message) {
  jQuery.when(message).then(function(messageString) {
    alert(messageString);
  });
}

This would handle values and promises of values correctly...

displayMessage("hello");                            // alerts "hello"
displayMessage(jQuery.Deferred().resolve("hello")); // alerts "hello"

...but once we get to promises of promises of values, it breaks down:

displayMessage(jQuery.Deferred().resolve(
  jQuery.Deferred().resolve("hello")
));                                                 // alerts "[object Object]"

jQuery.when is able to tell if a value is promise, so apparently it is possible. How can I check?

Jeremy
  • 1
  • 85
  • 340
  • 366
  • 1
    Take a look at the instanceof operator: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/instanceof – Heretic Monkey Oct 25 '12 at 19:15
  • 2
    @MikeMcCaughan Unfortunately there does not seem to be an exposed `Promise` type with which to use it. – Jeremy Oct 25 '12 at 19:18
  • Is `displayMessage` meant to be used on types other than strings/deferreds/promise? – zzzzBov Oct 25 '12 at 19:19
  • @zzzzBov Yes. This is a simplified example, and I realize it doesn't address that, but in my real code this is a general-purpose function that may handle data of any type. – Jeremy Oct 25 '12 at 19:20
  • @Swamp, then why not use two separate methods? One for deferred data-dumping, and another to do the data-dumping? – zzzzBov Oct 25 '12 at 19:21
  • @zzzzBov I'm mirroring the interface of some existing ActionScript code, and I'm afraid that this is how it has implemented it. – Jeremy Oct 25 '12 at 19:23
  • From the jQuery source, it looks like it just does `jQuery.isFunction(maybeADeferred.promise)`, much like what @zzzBov suggests in his answer. – Heretic Monkey Oct 25 '12 at 19:31

2 Answers2

32

jQuery.when is able to tell if a value is promise, so apparently it is possible.

This is mistaken. jQuery itself is not able to check if an object is a promise with complete accuracy. If you look at the source of jQuery.when in the jQuery source viewer you can see that all it does is this:

jQuery.isFunction(firstParam.promise)

If the object you are returning has its own .promise() method, jQuery.when will misbehave:

var trickyValue = {
  promise: function() { return 3; },
  value: 2
};

jQuery.when(trickyValue).then(function(obj) {
  alert(obj.value);
});

This throws TypeError: Object 3 has no method 'then', because jQuery assumes the object is a promise and trusts the value of its .promise() method.

This is probably impossible to solve properly. The promise object is created as an object literal inside of jQuery.Deferred (view source). It has no prototype, nor any other truly unique properties that could be used to distinguish it.

However, I can think of a hacky solution that should be reliable as long as only one version of jQuery is in use:

function isPromise(value) {
  if (typeof value === 'object' && typeof value.then !== "function") {
    return false;
  }
  var promiseThenSrc = String($.Deferred().then);
  var valueThenSrc = String(value.then);
  return promiseThenSrc === valueThenSrc;
}

isPromise("test");                 // false
isPromise($.Deferred());           // true
isPromise($.Deferred().promise()); // true

Converting a function to a string gives you its source code, so here I am comparing then source of the .then method of a new Deferred object to that of the value I'm interested in. Your value is not going to have a .then method with exactly the same source code as a jQuery.Deferred or Promise1.

1. Unless you're running in a hostile environment, in which case you should probably give up.


If you aren't specifically interested in jQuery promises, but would like to detect any type of Promise including the built-in ones from ECMAScript 6, you can test if value is an object and has a then method:

if (typeof value === 'object' && typeof value.then === 'function') {
  // handle a promise
} else {
  // handle a concrete value
}

This is the approach by several Promise-handling functions defined in ES6. You can see this described in the specification of the resolve(...) functions, partially quoted below:

When a promise resolve function F is called with argument resolution, the following steps are taken:

[...]

  1. If Type(resolution) is not Object, then
    1. Return FulfillPromise(promise, resolution).
  2. Let then be Get(resolution, "then").
  3. If then is an abrupt completion, then
    1. Return RejectPromise(promise, then.[[value]]).
  4. Let thenAction be then.[[value]].
  5. If IsCallable(thenAction) is false, then
    1. Return FulfillPromise(promise, resolution).
  6. Perform EnqueueJob ("PromiseJobs", PromiseResolveThenableJob, «‍promise, resolution, thenAction»)
Jeremy
  • 1
  • 85
  • 340
  • 366
  • 6
    I would upvote this, because I was just going to mention how jQuery does it behind the scenes, but I can't approve that solution because Promises can and should be creatable by other libraries, such as [Q](https://github.com/kriskowal/q) or [When](https://github.com/cujojs/when) (or even [WinJS](http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx)!), and your solution won't work with them. In my opinion, the jQuery solution is ok. Maybe, we could just check for the presence of more functions besides `.promise` (such as `.then`), that's enough I think. – Camilo Martin Nov 24 '12 at 03:45
14

The quick-and-dirty solution is to test if the object has a then function:

if (typeof message.then === 'function') {
    //assume it's a Deferred or fits the Deferred interface
} else {
    //do other stuff
}
zzzzBov
  • 174,988
  • 54
  • 320
  • 367
  • angularJS promises utilize then() method so this helped me. jquery's when() did not work in my case. – Adil F Dec 11 '13 at 23:06
  • 3
    This is a "thenable" concept clearly defined in Promises/A+ specification: http://promises-aplus.github.io/promises-spec/#point-7 – noitseuq Jul 08 '14 at 13:48
  • @Jeremy Banks, did you mean to significantly revise my answer? If so, it's common courtesy to at least leave a comment before doing so. – zzzzBov Apr 19 '16 at 17:42
  • I disagree, and do this often. Given the ease with which edits can be rolled-back, I think it's much more productive for users to feel empowered to make changes, to "edit boldly" in Wikipedia parlance, rather than worrying about stepping on an "owner"'s toes when we should be treating this as a community resource. Rather, I felt it was a courtesy to put this in your answer, because it was a clarification of the approach you had described, rather than trying to take the rep for myself. If you don't like the change, well, it only lived for eight minutes. – Jeremy Apr 19 '16 at 17:58
  • @JeremyBanks, to confirm, you actively disagree that it's a common courtesy to notify an author of *significant changes* you're making to their post which will affect *their* reputation, possibly negatively? I didn't say you shouldn't be making changes, I'm simply requesting that you reevaluate how you make them. – zzzzBov Apr 19 '16 at 18:14
  • wrt "we should be treating this as a community resource" in this case you already have an answer. If you want to elaborate, do so in your own answer. "...rather than trying to take the rep for myself" don't worry about taking rep for yourself. If it's your words attributed to you and you earn rep for it, you've earned it. – zzzzBov Apr 19 '16 at 18:17
  • Yes, I disagree with that. The system will notify you. – Jeremy Apr 19 '16 at 18:18