Make the most of 'promises' in JavaScript

Handling concurrency in distributed applications is a challenge. Here's how to use promises and keep everything on track

Make the most of promises in JavaScript
Pixabay (Creative Commons BY or BY-SA)

Once we coded applications that stood alone or were tightly coupled to specific servers. Now we’re writing applications that are loosely coupled, with app and browser front ends connected to distributed cloud services. This simplifies many things, but also causes new problems.

The biggest problem facing the modern application developer is one of the oldest in distributed computing: how to ensure consistency among the various components of an application. This question goes to the heart of delivering modern applications -- especially those made up of loosely coupled microservices.

JavaScript is a key technology in this new world, so it’s no surprise that in the next major revision of the underlying ECMAScript standard, features will be added to help handle consistency -- features that are already appearing in the latest browsers. At the heart of JavaScript’s asynchronous connection-handling are “promises,” which provide a new way of scheduling how your code will work with JavaScript APIs and functions.

The promise of promises

Promises are ECMAScript 6 objects that wrap specific actions, so they always either complete or are rejected.

Promise-like features in various JavaScript libraries have emerged previously to give JavaScript applications support for asynchronous connections. Some, such as RSVP.js and JQuery’s Deferreds, have been quite popular. Now that JavaScript has native support for promises, however, you can avoid hefty library downloads and use the APIs built into your users’ browsers. Of all the major browsers, only the legacy IE fails to handle promises (Windows 10’s Edge browser does handle them).

Perhaps the most important aspect of a promise is that it can change state only once, from pending to either success or failure. A promise that hasn’t been fulfilled or rejected is pending. If it succeeded it’s fulfilled; if it failed it’s been rejected (and you can hand it over to an error handler). If it succeeds, then it gets a value; if it’s rejected, you have to give a reason. What matters isn’t when the promise has been settled, it’s that it has.

Some JavaScript APIs and libraries already offer promises, so you don’t have to construct them by hand. The new Service Worker, Web Audio, and Streams APIs all support promises, and the intent is to support them in new APIs going forward. You’ll also find support in Angular 2 and other JavaScript frameworks.

A promise wraps a JavaScript function, treating it as an object with a set of new methods handled by the promise API. It looks at first glance much like any existing JavaScript callback, but the code can be a lot flatter, avoiding lots of nested brackets that complicate debugging. For example, if you’re logging in a user, you can build a promise that handles both log-in and getting user data all as one action.

Putting promises to work

There’s a lot you can do with promises. They’re perhaps best deployed as wrappers for callbacks -- or as a way of managing events more effectively, especially when you’re using Node.js as a message handler.

It’s useful that then has two states: one handling a promise that has been fulfilled and one handling a promise that has been rejected. Using then’s rejection handler makes it easy to build error handling into your application, using catch to handle an error response before stepping onto the next then in a chain of promises.

Promises are surprisingly powerful. You can chain them together as a string of then calls, so one response depends on the previous, and on and on. Events that require sequential operation can then be triggered by a callback, allowing them to operate asynchronously.

For example, you could use a chain of calls to load sections of a long document sequentially, pulling in the next section as a user scrolls down a page. Similarly, you could use promises to handle interactions in a single-page application, ensuring that each element of an application is processed appropriately -- reducing the risk of malformed or partial data being received from a server.

Handling rejection in a chain of promises also allows you to build recovery steps into your exception handling. The enables you to ensure all the states you capture in your promises are handled -- or at least managed.

One possible use for this approach is working with promises to wrap calls to APIs on different services, allowing you to ensure that correctly processed data is delivered to your application in a timely fashion. If a call to service 1 fails, it can be retried -- while you’re currently handling a call to service 2, then to service 3. It’s not quite multithreaded programming in JavaScript, but it’ll give you many of the same results.

Promises all in a row

It gets really interesting when you build an array of promises and call it with promise.all. The resulting promise is fulfilled only when all other promises have completed, with results in a matching array.

There’s no need to marshal those promises; they can operate at whatever speed they need. You’ll get your results no matter what and the resulting data can be displayed a lot more quickly than if you’d handled promises one by one. You can also write code that handles data as it’s returned, pulling out content out of the results array in sequence.

ECMAScript 6 adds further promise features, though they’re not currently supported by all browsers. As we transition to yearly ECMAScript releases with ECMAScript 2016, we’re going to see many more features added to support other forms of asynchronous and distributed computing.

Solving distributed computing problems isn’t easy, but with JavaScript’s role in microservices and in cloud applications, it’s going to become an increasingly important issue. Getting to grips with promises might only be a start, but it’s a step in the right direction.

Copyright © 2015 IDG Communications, Inc.