5

I think I have finally managed to bend my mind around javascript/ES6 Promises, for the most part. It wasn't easy! But there's something that's baffling me about the design.

Why does the Promise constructor take a callback? Given that the callback is called immediately, couldn't the caller just execute that code instead, thereby avoiding one unnecessary level of mind-bending "don't call me, I'll call you"?

Here's what I think of as the prototypical example of Promise usage, copied from Jake Archibald's Javascript Promises tutorial http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promisifying-xmlhttprequest , with comments stripped.

It's a Promise-based wrapper for an XMLHttpRequest GET request:

function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response);
      }
      else {
        reject(Error(req.statusText));
      }
    };
    req.onerror = function() {
      reject(Error("Network Error"));
    };
    req.send();
  });
}

For me, the above code would be much easier to understand if it were rewritten as follows, using a very slightly different kind of promise that I'm imagining, having a no-arg constructor and resolve/reject methods:

function get(url) {
  var promise = new MyEasierToUnderstandPromise();
  var req = new XMLHttpRequest();
  req.open('GET', url);
  req.onload = function() {
    if (req.status == 200) {
      promise.resolve(req.response);
    }
    else {
      promise.reject(Error(req.statusText));
    }
  };
  req.onerror = function() {
    promise.reject(Error("Network Error"));
  };
  req.send();
  return promise;
}

MyEasierToUnderstandPromise is not too hard to implement in terms of Promise. At first I tried making it an actual subclass of Promise, but for some reason I couldn't get that to work; so instead I implemented it as a simple factory function, which returns a plain old Promise object with a couple of extra functions attached that behave like member functions:

function NewMyEasierToUnderstandPromise() {
  var resolveVar;
  var rejectVar;
  var promise = new Promise(function(resolveParam, rejectParam) {
    resolveVar = resolveParam;
    rejectVar = rejectParam;
  });
  promise.resolve = resolveVar;
  promise.reject = rejectVar;
  return promise;
};

So, why isn't Promise designed like this? I think if it was, it would have helped me to understand Promises a lot quicker-- I bet it would have cut my learning time in half.

I know a lot of smart people had a hand in making the Promise API, and everyone seems to be generally happy and proud of it, so I'm wondering what they were thinking.

Don Hatch
  • 5,041
  • 3
  • 31
  • 48
  • 1
    your "easierToUnderstandPromise" is like jQuery.Deferred in a way. With your design, the returned promise necessarily exposes resolve/reject methods. I've read somewhere why this is a "bad thing", but I can't find that resource (it's been years since I read it) – Jaromanda X Feb 13 '16 at 03:31
  • Read about [ES7 async/await](https://jakearchibald.com/2014/es7-async-functions/). – Michał Perłakowski Feb 13 '16 at 19:47
  • There is also the deferred pattern, but [it's deprecated for good reason](http://stackoverflow.com/q/28687566/1048572) – Bergi Mar 06 '16 at 14:35

3 Answers3

6

Your version is not exception-safe, whereas Promises/A+ are safe since they are caught by the Promise constructor.

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Great answer, thank you. I think all the introductory docs and tutorials would be greatly improved by mentioning this-- my brain just doesn't want to absorb things for which it can't see a reason! – Don Hatch Feb 13 '16 at 03:43
5

Promises are intended to be used as values. The ES constructor approach encapsulates the creation of the Promise, which can then be passed around like a value. When that value is passed around, the consumer of that value has no need for resolve and reject and so those functions should not be part of the public API.

(and all that stuff about exception handling and chaining)

rich remer
  • 3,407
  • 2
  • 34
  • 47
1

As ancilliary information, when a script defines an ES6 promise chain like

var promise = new Promise(executor).then(something).catch(handleError);

the promise variable is left set to the promise returned by the .catch method call. The entire chain or Promise objects is actually prevented from being garbage collected by the resolve/reject function references held by the executor function. If resolve is called (usually asynchronously after the executor returns but prior to letting resolve/reject functions go out of scope), then listener "something" is called, with a Promise platform internal executor holding resolve/reject function references for the promise returned by the then call to prevent it and any following chained promises from themselves being garbage collected prematurely.

Under the deferred model suggested you can't set up the promise chain in this way because you need a reference to the first promise in the chain in order to resolve or reject it. The code becomes more like

var promise = new Promise(); // no executor
promise.then(something).catch(handleError);
initiateOperation( promise);

and then asynchronously in operation response code

promise.resolve(value);  // if no error occurred
promise = null;          // explicit discard of first promise reference to allow GC?

The general minimilistic approach of ES6 promises now starts to look promising (ouch). I do sympathize with your difficulty in learning how promises work - an interesting journey!!!

traktor
  • 17,588
  • 4
  • 32
  • 53
  • 1
    No, you don't need to explicitly discard references to `promise` in the async callback, just like you don't need to explicitly discard references to `resolve`/`reject` in the executor scope. – Bergi Mar 06 '16 at 14:37