Using Promises with the JavaScript Diffusion Client
March 6, 2020 | Holger Schmitz
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 Result
s 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.
- connect to the Diffusion server and obtain a session
- create a new topic
- send a message to a recipient and obtain an answer
- 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 Promise
s 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.
Further reading
BLOG
Creating a WebSocket Server for PubSub
June 28, 2024
Read More about Creating a WebSocket Server for PubSub/span>
BLOG
Benchmarking and scaling subscribers
March 15, 2024
BLOG
Unlocking the Value of ISO 27001 Certification: A Journey of Security and Continuous Improvement
March 25, 2024