Rico Suter's blog.
 


JavaScript Promises are a very powerful tool to avoid the callback nesting hell. The following article describes some tips and tricks when working with Promises. For developers with C# async/await knowledge, I will also show the async/await representations. With TypeScript, you can use this async/await syntax today.

Most modern browsers only support JavaScript ES5 which does not provide a native Promise implementation. This is why you need one of the 3rd party Promises frameworks. When choosing a framework, you should check that it follows the Promises/A+ specification or a polyfill. The samples in this article are implemented using the Q Promises library.

Use then to run code after a Promise has completed

To run code after a Promise has completed, call its then method and register a fulfilled or failed callback:

function () {
    var promise = delay(1000);
    promise.then(function() {
        // Add success handler code 
    }, function() {
        // Add failed handler code 
    }).done();
}

The previous code would look like this with async/await:

async function () {
    var promise = delay(1000);
    try {
        await promise;
        // Add success handler code 
    } catch {
        // Add failed handler code 
    }
}

A then fulfilled callback can return a value or Promise or throw an exception

There are tree ways to complete a then fulfilled or failed callback:

Return a value

If the fulfilled callback returns a value, then the Promise is immediately resolved with this value after the previous Promise has completed:

function () {
    return delay(1000).then(function () {
        return "hello world";   
    });
}

Function implementation with async/await:

async function () {
    await delay(1000);
    return "hello world";
}

Throw an exception

If the fulfilled callback throws an exception, then the Promise is immediately rejected after the previous Promise has completed:

function () {
    return delay(1000).then(function () {
        throw "my custom exception";    
    });
}

Function implementation with async/await:

async function () {
    await delay(1000);
    throw "my custom exception";
}

Return a new Promise

If the fulfilled callback returns another Promise, then the then call returns the returned Promise:

function () {
    return delay(1000).then(function () {
        return http.get("my/path");     
    });
}

Function implementation with async/await:

async function () {
    await delay(1000);
    return await http.get("my/data");
}

Either return the Promise or call done

Use then when the Promise is returned

If your method is called by a Promise-aware callee, then you just return the Promise:

function downloadAndTransform () {
    return http.get("my/data").then(function (data) {
        return transform(data);
    });
}

Use done when the Promise is not returned

(This section only applies if your Promise implementation provides a done method)

However, if you don’t return the Promise, you need to call done. Usually, you only need to call done in a event callback whose caller does not expect a Promise but void:

function () {
    $("#target").click(function () {
        http.get("hello/message").done(function(message) {
            alert(message);            
        });
    });
}

This way, if an exception occurs somewhere in the then-callstack, the exception is finally thrown on the UI thread and thus visible in the console. It is not allowed to return a value or Promise in one of the done() callbacks because the function will not return a continuation Promise.

Use catch instead of then(null, rejected)

Lets have a look at the following function which loads an HTTP resource, transforms it and posts it back:

function loadTransformAndSave() {
    return http.get("my/path").then(function (data) {
        var data = transform(data);
        return http.post("my/path", data);
    }).then(function () {
        alert("Data transformed and saved. ");
    }).catch(function (reason) {
        alert("Exception: " + reason);
    });
}

The then fulfilled callbacks are skipped in case of a previous rejection. In the sample, I used the catch(rejected) method which most Promises frameworks provide as a replacement for then(null, rejected). The catch method has only one parameter: A callback which is called when a previous Promise got rejected.

To better understand the previous code, just have a look a the async/await representation of the function:

async function loadTransformAndSave() {
    try {
        var data = await http.get("my/path");
        data = transform(data); 
        await http.post("my/path", data); 
        alert("Data transformed and saved. ");
    } catch(reason) {
        alert("Exception: " + reason);        
    }
}

Using catch instead of then(null, rejected) makes your code much easier to read.

The call to catch returns a resolved Promise

The call to catch (or then(null, rejected)) catches an exception and returns a new fulfilled or rejected Promise. An example:

function loadWithFallbackValue() {
    return http.get("my/path").catch(function (reason) {
        return "No data found";
    }).then(function (data) {
        alert(data);
    });
}

The async/await representation of the previous code:

async function loadWithFallbackValue() {
    var result = undefined; 
    try {
        result = await http.get("my/path");
    } catch (reason) {
        result = "No data found";
    }
    alert(result);
}    

An async function must always return a Promise

If a function is asynchronous, it must always return a Promise even if the function returns synchronously. This way the caller of the function has always a then function to chain the Promise. Have a look a the following function:

function getPersonById(id) {
    if (id === undefined || id === null)
        return Promise.reject(new Error("No id given. "));

    return http.get("/Person/" + id);
}

As you can see, the call Promise.reject(exception) creates an already completed but rejected Promise. If you need a completed and resolved Promise, just call Promise.resolve(data).

Avoid nesting in callbacks

When you chain multiple Promises, you may be tempted to return a Promise chain inside a then callback:

return askValue().then(function (value) {
    return a(value);
}).then(function (value) {
    return b.then(function (value) {
        return c(value);
    });
}).then(function (value) {
    return d(value);
});

I consider this bad pracise as you should always directly return the Promise, and react to its completion one level deeper:

return askValue().then(function (value) {
    return a(value);
}).then(function (value) {
    return b(value);
}).then(function (value) {
    return c(value);
}).then(function (value) {
    return d(value);
});

With async/await, the previous chain will be written as:

var value = await askValue();
value = await a(value);
value = await b(value);
value = await c(value);
return await d(value);

Use Promise control flow helper functions

You should use the available flow helper functions to work with Promises:

Promise.all

Wait for multiple Promises:

var p1 = http.get("my/first/path");
var p2 = http.get("my/second/path");

Promise.all([p1, p2]).then(function (values) {
    processValues(values);
});

With async/await, the previous code will be written as:

var p1 = http.get("my/first/path");
var p2 = http.get("my/second/path");

var results = await Promise.all([p1, p2]);
processValues(values);

Use Promises with 3rd-party libraries

Wrap JQuery Promises

If you use JQuery for HTTP calls, you have convert the JQuery Promise into a “native” Promise instance. This can be achieved in the following way:

var promise = new Promise(function (reject, resolve) {
    $.ajax({ ... }).done(function (data) {
        resolve(data);
    }).fail(function (xhr) {
        reject(xhr);
    });
});

The specification

Now that you have seen some basic samples, you should read the Promises/A+ specification (it is pretty short).



Discussion