Search code examples
ember.jsrsvp.js

How can I test if a function is returning a promise in Ember?


In Ember, I sometimes run into the situation where I need to check if a function is returning a promise. For example, if I have a route that is derived:

MyRoute = ParentRoute.extend({
  beforeModel: function() {
    this._super().then(function() {
      // do something...
    });
  }
});

But although beforeModel can return a promise, it might not. In particular, if it's the default Ember.K implementation, then it doesn't. I'd rather not always be doing:

var res = this._super();
if (typeof res.then === "function") {
  res.then(function() {
    // do X
  });
} else {
  // do X
}

I assume there's a way to wrap something that one doesn't know if it's a thenable, and then chain regardless. But I couldn't find it in the documentation.

The above is undesirable because it's verbose, and requires having the code for X twice.

Thoughts?

Update:

I was able to confirm @torazaburo's response with the following coffeescript test:

`import { test, module } from 'ember-qunit'`

module "testing promise behavior"

test "can cast to promise", ->
  expect 3
  order = []
  returnsPromise = ->
    new Ember.RSVP.Promise (resolve) ->
      order.push 'a'
      resolve('response 1')
  returnsValue = ->
    order.push 'b'
    'response 2'

  Ember.run ->
    Ember.RSVP.resolve(returnsPromise()).then (response) ->
      order.push 'c'
      equal response, 'response 1'
    Ember.RSVP.resolve(returnsValue()).then (response) ->
      order.push 'd'
      equal response, 'response 2'

  equal order.join(' '), 'a b c d'

Thanks for the solution! It seems RSVP's implementation of Promises also has a built in resolve method that does what you suggest, and it turns out to be the same as cast, as you suggest, althought that is now deprecated.


Solution

  • There may be other better ways to do this, but you could do:

    function ensurePromise(x) {
        return new Ember.RSVP.Promise(function(resolve) {
            resolve(x);
        });
    }
    

    If x is not a promise, then this returns a promise which is already fulfilled with that value, which you can then hang then's off of. If x is a promise, then it returns a promise which assumes its status (including resolved/rejected status and value/reason).

    This is equivalent in native Promises to

    Promise.resolve(x)
    

    So in your case,

    MyRoute = ParentRoute.extend({
        beforeModel: function() {
            ensurePromise(this._super()).then(function() {
                // do something...
            });
        }
    });
    

    Note, however, that this will potentially turn a synchronous value into an asynchronous value (a promise). However, it is generally considered bad practice to have functions that behave synchronously in some cases and asynchronously in others. So it seems OK, in the case we have a value which is potentially either synchronous or asynchronous, to coerce it into something which is always asynchronous.

    I believe in some past version there used to be something called RSVP.Promise.cast, which I seem to recall did roughly the same thing, but I can't track it down now.