3
\$\begingroup\$

I created this demo example for MDN documentation to show how to use Promise.all and Promise.defer in user created promise. I used lets because I thought if I didn't it would mess it up.

People are recommending I avoid let because the standard is not there yet and it looks like when it lands it will be a little different. Can you please advise?

The code downloads images from an array and after all images are downloaded it completes the main promise.

Components.utils.import("resource://gre/modules/Promise.jsm");

var myPromise = myUserDefinedPromise();
myPromise.then(
  function(aSuccessReason) {
    alert('myPromise was SUCCESFUL and reason was = "' + aSuccessReason + '"');
  },
  function(aRejectReason) {
    alert('myPromise FAILED for reason = "' + uneval(aRejectReason) + '"');
  }
);

function myUserDefinedPromise() {
  try {
    var mySubPromises = [];

    var imagePaths = ['http://www.mozilla.org/media/img/firefox/favicon.png', 'https://developer.cdn.mozilla.net/media/redesign/img/favicon32.png'];
    [].forEach.call(imagePaths, function(path) {
      let myImage = new Image();
      let loadThisImagePromise = Promise.defer();
      mySubPromises.push(loadThisImagePromise.promise);
      myImage.onload = function() {
        loadThisImagePromise.resolve('Succesfully loaded image at path = "' + path + '" the width of this image is = "' + this.naturalWidth + '".');
        if (!this.naturalWidth) {
          loadThisImagePromise.reject('Image loaded but it has 0 width at path = "' + path + '" the naturalWidth was 0');
        }
      }
      myImage.onerror = function(e) {
        loadThisImagePromise.reject('An error occured while loading path = "' + path + '". The error = ' + uneval(e));
      }
      myImage.onabort = function(e) {
        loadThisImagePromise.reject('Image load was aborted loading path = "' + path + '".');
      }
      myImage.src = path;
    });

    return Promise.all(mySubPromises);
  } catch(ex) {
    return Promise.reject(ex);
  }
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Regarding var vs. let

People are recommending I avoid let because the standard is not there yet and it looks like when it lands it will be a little different. Can you please advise?

Regarding your comment: "People"'s opinion on var vs. let is a bit off... It makes sense for the web not to use let just yet as not all browsers support let.

But within the mozilla code base let is actually already used since years and pretty stable. There are more than 83.000 lets in mozilla-central (via ag -w let | wc -l). Brendan Eich (I think) coined the term "let is the new var" (for mozilla-specific code) way back when JS1.7 was released a part of Firefox 2 in 2006. Ever since, it has seen wide-spread adoption within the mozilla code base and add-ons alike.

But, since you're writing documentation, it would be better to follow the lead of the main MDN contributors, even if you write mozilla-specific examples aimed at platform and add-on developers. I'm not sure what the MDN policy is on this... I wrote a bunch of mozilla-specific examples in the past for MDN, and used let and other stuff like generators, and never was told to stop that. But maybe I just wasn't caught.

Replacing one with the other

You asked about this in a comment, but I think it would be more appropriate to answer here.

In general you can replace var with let in most cases, except where you really want the hoisting rules. E.g. one example where stuff would break.

try {
  var name = getName();
} catch (ex) { ... }
console.log(name);

But with let, name will be only valid within the try-block.

try {
  let name = getName();
} catch (ex) { ... }
console.log(name); // Either undefined or an error, depending on strict-mode.

You could still write this as (and I guess it would have been a good idea to write it like this in the first place, even with var).

let name;
try {
  name = getName();
} catch (ex) { ... }
console.log(name); // Either undefined or an error, depending on strict-mode.

The other way round is more tricky. Don't do it blindly. Be aware of name clashes it may create due to different scoping/hoisting rules, and what scoping difference means for "global" code in particular.

A problematic example:

for (var i = 0; i < 10; ++i) {
  let p = i * 10;
  setTimeout(function() { console.log(p) });
}
// 0, 10, 20, 30, 40, 50, 60, 70, 80, 90

Using var p is wildly different because p gets hoisted outside of the loop block while let p is hoisted within the block (JS1.7) or not-really-hoisted-but-kinda (ES6):

for (var i = 0; i < 10; ++i) {
  var p = i * 10;
  setTimeout(function() { console.log(p) });
}
// 90, 90, 90, 90, 90, 90, 90, 90, 90, 90

JS1.7 to ES6 differences.

The hoisting difference of let is the most important practical difference between JS1.7 and ES6 let.

(function() { console.log(i); let i = 0; })();

This will log undefined in JS1.7, but should throw an error in ES6 because i was referenced before it actually was declared.

Since you should avoid to write such code anyway, it shouldn't really make much difference in practice, except to expose the occasional bug once the engine switches from JS1.7 to ES6 behavior.

There are some other differences (e.g. no more let-blocks), but I'd consider them extremely minor in practice and don't think these differences would affect lots of production code.

Regarding Promise.jsm documentation and your code.

Well, I don't really think the example is appropriate:

  1. It relies on the network.
  2. It relies on some external resources, which may get removed in the future at any point.
  3. It isn't simple enough.
  4. It is incorrect when put into a strict-mode function.

If write an example like this, keeping it as simple as possible.

Components.utils.import('resource://gre/modules/Promise.jsm');

/**
 * Example function, just returning a promise that
 * will resolve or reject after a timeout.
 */
function createTimeoutPromise(shouldSucceed, timeout) {
  var p = Promise.defer();
  setTimeout(function () {
    if (shouldSucceed) {
      p.resolve(timeout);
    } 
    else {
      p.reject(timeout);
    }
  }, timeout);
  return p.promise;
}

/**
 * Success case.
 */
(function() {
  var promises = [];
  for (var i = 0; i < 10; ++i) {
    promises.push(createTimeoutPromise(true, i * 100));
  }
  var combined = Promise.all(promises);
  combined.then(
    function onResolved(result) {
      console.log('resolved', result);
    },
    function onRejected(result) {
      console.log('rejected', result);
    });
  // Since all promises eventually resolve/succeed, onResolved will be called.
  // Promise.all will pass an array of all resolve() results.
  // This hence logs:
  // "resolved" Array [ 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 ]
})();

/**
 * Failure case.
 */
(function() {
  var promises = [];
  for (var i = 0; i < 10; ++i) {
    // Odd numbered promises should fail: i % 2 == 0
    promises.push(createTimeoutPromise(i % 2 == 0, i * 100));
  }
  var combined = Promise.all(promises);
  combined.then(
    function onResolved(result) {
      console.log('resolved', result);
    },
    function onRejected(result) {
      console.log('rejected', result);
    });
  // Since odd numbered promises will be rejected/fail, onRejected will be called.
  // Promise.all will immediately reject the returned combined promise once the
  // first sub-promise fails, i.e. promise "100".
  // Other resolved and rejected promises won't be passed.
  // This hence logs
  // "rejected" 100
})();

I'm not quite sure how much of a point there is maintaining Promise.jsm documentation, since ES6 Promises landed in the meantime. I'd expect that Promise.jsm will get phased out at some point.

\$\endgroup\$
3
\$\begingroup\$

Here's my implementation:

// So we define a function that loads images, and returns a promise for us to listen to
function loadImages(images){

  // We can take advantage of `map` which iterates through an array and makes an array
  // out of the returns of each iteration. In this case, we return a promise for each
  // url, for `Promise.all` to listen to later.
  var promises = images.map(function(url){

    // Create a Deferred object and image, and handle the resolution stuff.
    var deferred = Promise.defer();
    var image = new Image();
    image.onload = function(){deferred.resolve('image success: '+ url);};
    image.onerror = function(){deferred.reject('image failed: ' + url);};
    image.onerror = function(){deferred.reject('image aborted: ' + url);};
    image.src = url;

    // Return the promise, which will be stored in the array, thanks to `map`
    return deferred;
  });

  // Listen on the array of promises (aha moment: jQuery.when == Promises.all, more or less.)
  return Promise.all(promises);
}

// Now we use `loadImages`, loading an array of urls, and listening for notices
loadImages([
  'http://www.mozilla.org/media/img/firefox/favicon.png',
  'https://developer.cdn.mozilla.net/media/redesign/img/favicon32.png'
]).then(function(value){
  console.log('loadImages successful: ' + value);
},function(error){
  console.log('loadImages failed: ' + error);
});

Additional notes:

  • Don't do [].forEach.call. Aside from looking very hackish, it creates an unnecessary array. Do Array.prototype.forEach.call instead.

  • I'd avoid using try-catch. While it's a good measure for errors, it's bad for optimization. Some optimizing compilers (I think V8) can't optimize code inside try-catch.

  • Name your variables and functions properly and verbosely.

\$\endgroup\$
4
  • \$\begingroup\$ Oh my gosh so simple and sweet. I had to make some corrections. Please see here: GitHubGist :: Revisions. And lastly you see that I added a commented out line alert(blah), if you uncomment this, how can you make it trigger the error function of the loadImages promise without a try-catch? Please see my comments here for more explanation on the revisions: GitHubGist :: Comments on Revisions \$\endgroup\$
    – Noitidart
    Jul 12, 2014 at 22:52
  • \$\begingroup\$ Thanks very much for your comments within the code, especially that aha moment on jQuery.when :) I also just learned the map function I never understood it. Thanks! \$\endgroup\$
    – Noitidart
    Jul 12, 2014 at 22:58
  • \$\begingroup\$ @Noitidart Ahh, sorry about the mistakes. I used promises before I decided to use Promise.defer. Hehehe. \$\endgroup\$
    – Joseph
    Jul 13, 2014 at 2:22
  • \$\begingroup\$ @Noitidart Regarding the alert, I don't know. I think the only way to trigger the fail function is to reject the promise. \$\endgroup\$
    – Joseph
    Jul 13, 2014 at 2:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.