Using Promises with the JavaScript Diffusion Client

Introduction

Many functions in the JavaScript Diffusion client return a Result object. This object can be used to obtain the return values of asynchronously executed code. A Result implements a then() method which takes one or two callback functions. The first callback function will be called with the return value once the asynchronous call has completed. The second callback function will be called in case of an error.

It is important to note that Result implements the full ES6 Promise specification and is in all respects equivalent to a Promise. This article is intended to shed light on best practices when using Results returned from the various Diffusion function.

The Callback Trap

When looking at the then() handler in terms of traditional callback functions, it is tempting to recursively wrap callbacks inside one another. This can result in unreadable, and potentially faulty code. Consider the following example. A function should run four tasks in sequence.

  1. connect to the Diffusion server and obtain a session
  2. create a new topic
  3. send a message to a recipient and obtain an answer
  4. set the previously created topic with the value received in the message response

Using traditional callback style, this could be written as follows.

function callback_pyramid() {
    diffusion.connect({
        principal : '',
        credentials : ''
    }).then(
        function(session) {
            session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING).then(
                function() {
                    session.messages.sendRequest('/path/to/message/topic', 'foo').then(
                        function(value) {
                            session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value).then(
                                function(value) {
                                    console.log("topic has been updated to "+value);
                                },
                                // Error callback for session.topics.set
                                function (error) {
                                    console.log(error);
                                }
                            )
                        },
                        // Error callback for session.messages.sendRequest
                        function (error) {
                            console.log(error);
                        }
                    );
                },
                // Error callback for session.topics.add
                function (error) {
                    console.log(error);
                }
            );
        },
        // Error callback for diffusion.connect
        function (error) {
            console.log(error);
        }
    );
}

This code has a number of problems. Every additional step in the sequence will create another level of nested callbacks. This makes the code extremely unwieldy and hard to read. In addition to this an error callback has to be provided at each level. The order of the error handlers is reversed with respect to the order of the actions performed making it very hard to visually associate the error handler with the call that caused the error.

Using Promises

The example above can be improved considerably. The Promise A+ specification states that then returns another Promise. If the function passed to the then() handler returns a value, the Promise will resolve with that value. If, on the other hand, that value returned by the handler is a Promise then the Promise returned by then() will resolve when the Promise returned by the handler resolves. While this sounds complicated, it means that it is possible to chain then() functions. Each member in the chain will only be executed once the previous members have completed.

The same example as above can now be written as follows.

function chaining_promises() {
    diffusion.connect({
        principal : '',
        credentials : ''
    }).then(
        function(session) {
            return session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING);
        }
    ).then(
        function() {
            return session.messages.sendRequest('/path/to/message/topic', 'foo');
        }
    ).then(
        function(value) {
            return session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value);
        }
    ).then(
        function(setValue) {
            console.log("topic has been updated to "+setValue);
        }
    ).catch (
        // Combined error callback
        function (error) {
            console.log(error);
        }
    );
}

Note how each handler passed to a then() function returns a Result object. Only when this Result resolves with a value will the next then() handler in the chain be called. The advantage of this approach is obvious. The sequence of actions results in a flat, seuential structure. An addition advantage is that an error in any member of this chain can be caught in a single error handler. The Promise returned by then() has a catch() method. The handler passed to this method will be called if any of the actions in the previous steps fails.

Using async/await

A relatively modern feature of JavaScript introduced in ES6 is the introduction of the async and await keywords. A function can be declared asynchronous by prefixing it with async. This indicates that the function implicitly will return a Promise. Inside the asynchronous function, calls to functions that return Promises can be prefixed with the await keyword. When the function is called, await will cause execution to pause until the Promise resolves. This allows the above example to be further simplified.

async function using_async_await() {
    try {
        const session = await diffusion.connect({
            principal : '',
            credentials : ''
        });

        await session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING);

        const value = await session.messages.sendRequest('/path/to/message/topic', 'foo');
        const setValue = await session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value);

        console.log("topic has been updated to "+setValue);
    }
    // Combined error callback
    catch(error) {
        console.log(error);
    }
}

The error handling now takes place using what looks like a traditional try-catch block. The amount of boiler plate code is reduced and the statements present themselves in a way that resembles synchronous sequential execution. This results in even more manageable code.

Summary

The use of Promises in JavaScript can greatly reduce the complexity of asynchronous code. By replacing a recursive wrapping of callbacks into on another with a more sequential approach, readability and maintainability is improved. The introduction of async and await improves on this even more. The Result object returned by many functions in Diffusion implements the Promise A+ specification and naturally fits into this paradigm.