Promises

Promise implementation of choice: Bluebird

Better ⇢ Reasonable#

// not good 
var p1 = api1();                // returning a promise 
var p3 = p1.then(api1Result => {
    var p2 = api2();            // returning a promise 
    return p2;                  // The result of p2 
});                             // becomes the result of p3 
 
// also not good 
api1().then(function(api1Result) {
    return api2().then(console.log)
})
 
// better 
api1().then(function(api1Result) {
    return api2();
}).then(console.log)
 
// best 
api1().then(api2).then(console.log);
 
// concurrently, logs both results 
Bluebird.join(api1(), api2(), (resut1, result2) => console.log(result1, result2));
// AKA 
Bluebird.join(api1(), api2(), console.log);

Anti-Patterns#

Anti-Patterns: Explicit Construction – Promises Everywhere#

// bad 
return new Promise(function(resolve) {
    getOtherPromise().then(function(result) {
        resolve(result.property.example);
    });
});
// good 
return getOtherPromise().then(function(result) {
    return result.property.example;
});

Word on the street: Create new promises sparingly; occasions that warrant constructing them are rare.

Anti-Patterns: .then(success, fail)#

.then(onSuccess, onRejected) has onRejected, but don't use it

Use .catch(...) instead.

The .then signature is mostly about interop, there is almost never a reason to use .then(success, fail) in application code.

Things To Avoid#

Avoid: Unnecessary .then(...)#

// bad 
return fetchSomething(id)
    .then(resp => {
        return resp.body;
    })
    .then(view => {
        cache[view._objectId] = view;
        return view;
    });
// good 
return fetchSomething(id)
    .then(resp => {
        const view = resp.body;
        cache[view._objectId] = view;
        return view;
    });

Avoid: Trailing .done()#

// bad 
return fetchSomething(id)
    .then(resp => {
        const view = resp.body;
        cache[view._objectId] = view;
        return view;
    }).done();
// good 
return fetchSomething(id)
    .then(resp => {
        const view = resp.body;
        cache[view._objectId] = view;
        return view;
    });

Avoid: Using .done(...) – ever#

Like .then, but any unhandled rejection that ends up here will crash the process (in node) or be thrown as an error (in browsers). The use of this method is heavily discouraged and it only exists for historical reasons.

Avoid: Returning Deadzone Promises#

Deadzone promise: (original term) - A promise that has no value to resolve to.

This happens when a .then(...) handler does not return a value but the promise is retained for further use or returned from a function.

// bad 
function malo() {
    return fetchSomething(id)
        .then(resp => {
            const view = resp.body;
            cache[view._objectId] = view;
        });
}
// good 
function bueno() {
    return fetchSomething(id)
        .then(resp => {
            const view = resp.body;
            cache[view._objectId] = view;
            return view;
        });
}
// also good 
function muyBueno() {
    fetchSomething(id)
        .then(resp => {
            const view = resp.body;
            cache[view._objectId] = view;
        });
}

Ignore this when... Deadzone promises are fine if you're interested in when an async process finished and do not care about the exact result.

// Source: http://jakearchibald.com/2014/es7-async-functions/ 
// Source: http://bluebirdjs.com/docs/api/each.html 
function loadStory() {
    return getJSON('story.json')
      .then(function(story) {
          addHtmlToPage(story.heading);
          return story.chapterURLs.map(getJSON);
      })
      .each(function(chapter) { addHtmlToPage(chapter.html); })
      .then(function() { addTextToPage("All done"); })
      .catch(function(err) { addTextToPage("Argh, broken: " + err.message); })
      .then(function() { document.querySelector('.spinner').style.display = 'none'; });
}

A Few Highlights From The API#

.map() | Bluebird.map(...) concurrency option#

Bluebird.map(input: Array<any> | Promise<Array<any>>, mapper: function(item: any) => Promise<any> [, options: {concurrency: int=Infinity}]) -> Promise
 
// But, basically, this: 
Bluebird.map(input: Array<any>, mapper: (any) => Promise<any> [, options: {concurrency: int}]) -> Promise

The {concurrency: int} option defines the maximum concurrent promises that will be created.

const typeIds = [...]; // 50 type IDs 
const fetchRuntimeView = this.props.utils.fetchRuntimeView;
 
// Typical approach, 50 concurrent HTTP calls 
return Bluebird.all(typeIds.map(fetchRuntimeView));
 
// An alternative -- Make 5 concurrent HTTP calls, then the 6th when one of them 
// finishes, 7th when another finishes, etc 
return Bluebird.map(typeIds, fetchRuntimeView, { concurrency: 5 });
 
// Makes 1 concurrent HTTP calls, i.e. sequential 
return Bluebird.map(typeIds, fetchRuntimeView, { concurrency: 1 });

.catch(...) – Filtered Variant (version 3)#

.catch(ErrorClass: class_ | predicateFn: function(error: any): boolean | predicateObject: Object..., handler: function(error: any)) -> Promise
  • ErrorClass needs to be a class constructor, and the documentation states that it needs to pass ErrorClass instanceof Error.
  • predicateFn needs to be a function that returns a boolean, with true meaning the handler function should be called.
  • predicateObject is a pattern matching object. Note: loose equality is used.

Regarding predicateObject:

The object predicate passed to .catch in the above code ({code: 'ENOENT'}) is shorthand for a predicate function function predicate(e) { return isObject(e) && e.code == 'ENOENT' }, I.E. loose equality is used.

// ErrorClass: class 
somePromise
    .then(function() {
        return a.b.c.d();
    })
    .catch(TypeError, function(e) {
        // If it is a TypeError, will end up here because 
        // it is a type error to reference property of undefined 
    })
    .catch(ReferenceError, function(e) {
        // Will end up here if "a" was never declared at all 
    })
    .catch(NetworkError, TimeoutError, function(e) {
        // List multiple error types 
    })
    .catch(function(e) {
        // Generic catch-the rest 
    });
 
// predicateFn: function(error: any): boolean 
const isClientError = err => err.code >= 400 && err.code < 500;
 
request("http://www.google.com")
    .then(...)
    .catch(isClientError, err => {
        // A client error like 400 Bad Request happened 
    });
 
// predicateObject: Object 
request("http://www.google.com")
    .then(...)
    .catch({code: 'ENOENT'}, function(e) {
        console.log("file not found: " + e.path);
    });

.tap(...)#

.tap(handler: function(value: any): Promise?) -> Promise

A utility method that allows you to handle a resolved value without being required to return anything.

.tap(...) is for side effects, only. It is not expected to return a value, and in-fact, returned values are pseudo ignored – if the handler returns a promise, the promise is awaited, but it does not affect the resolution value.

doSomething()
    .then(...)
    .then(...)             // Whatever is returned here 
    .tap(console.log)      // Will be logged 
    .then(...)             // And then will also be the param here 
    .then(...)

.each(...) | Bluebird.each(...)#

.each(iterator: function(any item, int index, int length): Promise?) -> Promise

Like .tap(...) but for arrays. It is not expected to return a value, and in-fact, returned values are pseudo ignored – if the handler returns a promise, resolution is paused until that promise resolves, but the promise does not affect the resolution value.

Resolves to the original array unmodified, this method is meant to be used for side effects.

// Cache results 
const typeIds = [...]; // 50 type IDs 
const fetchRuntimeView = this.props.utils.fetchRuntimeView;
return Bluebird
    .map(typeIds, fetchRuntimeView, { concurrency: 5 })
    .each(view => cache[view._objectId] = view);