JavaScript Promises – How They’ll Work Someday

In my last two posts, I showed you how JavaScript Promises, an ES6 API that streamlines and simplifies asynchronous programming, work—and how they can break.

In this final post in the series, I will show you how you can reduce the pain of working with Promises using new JavaScript language features–if your target environment supports them.

This is the 3rd is a series on JavaScript Promises:

Arrow Functions

If you’ve been working with Promises for a long time, you’re almost certainly very tired of typing (and reading!)

.then(function(argument) {
    return nextFunction(argument, etc);
})

ad nauseam. But if your target environment supports ES6 arrow functions, you can write this same .then() like this:

.then((argument) => {
    return nextFunction(argument, etc);
})

Or even like this:

.then(argument => nextFunction(argument, etc))

Arrow functions are ES6’s best feature (as has been written about on Spin before), and they can help reduce your reams of Promise chains to much more concise blocks of code.

async and await

Arrow functions are great, but the final forms of Promise are the upcoming async and await keywords. They work much like similarly-named support in other languages, and they work with Promises.

Here’s a moderately complicated Promise chain using just ES6 features. It has to carry forward variables through the chain by using closures, the best method available (regrettably, since they’re something that gets really ugly as those variables multiply):

function printShippingLabel(personId) {
    let person;

    return getPerson(personId)
        .then((_person) => {
            person = _person;
            return getAddress(personId, AddressType.PERSON);
        })
        .then((address) => printLabel(person, address));
}

If your environment supports async and await, this code can be transformed thus:

async function printShippingLabel(personId) {
    const person = await getPerson(personId);
    const address = await getAddress(personId);
    await printLabel(person, address);
}

Much like the Python asyncio library I wrote about a year and a half ago (Python has since adopted its own async and await in 3.5), JavaScript’s await effectively suspends the execution of a function declared async until the result of the expression is available. Using this scheme not only frees us from using a closure to carry forward the value of person between .then() blocks; it also allows us to use const to declare values as final. This prevents them from being accidentally reset later—definitely a win for code quality.

await can be used to wait on any other function declared async, or any function that returns a Promise. Conversely, a function declared async can be used with the Promise API—just call it in a non-async context and attach .then()s and .catch()es to it. We also get exceptions back:

async function printLabel(person, address) {
    const printResult = await submitLabelJob(person, address);
    if (printResult) {
        throw new Error('job submit failed: ' + printResult);
    }
}

async function printShippingLabel(personId) {
    const person = await getPerson(personId);
    const address = await getAddress(personId);
    try {
        await printLabel(person, address);
    } catch(printError) {
        console.error('caught print error', printError)
    }
}

Exceptions work exactly as you expect them to when you’re in async/await code, and they can be caught with .catch() in Promise-based code. The friction of programming this way is so low that you could almost forget you’re working with asynchronous tasks. Almost.

The Future Looks Bright (But It’s Still the Future)

Unfortunately, ES7 (and even some of ES6) isn’t necessarily something we can target today. Arrow functions are available in modern browsers and versions of Node, but they’re not everywhere. async and await are available pretty much nowhere.

There are tools that will provide access to all of this without giving up your compatibility, though. For experimenting with async and await, I used Facebook’s regenerator (which also supports async and await in addition to generator functions). Babel can wrap regenerator and its own transformation of arrow functions into JavaScript that’s compatible with older runtimes.

It’s been a long road from event spaghetti to where we are today. With Promises, there’s a workable system in place. And with async and await coming down the road, promising to finally make JavaScript asynchronous programming as straightforward as it should be, the future looks even brighter.


This is the 3rd is a series on JavaScript Promises:

 
Conversation
  • DataGrump says:

    I still haven’t been able to bring myself to use promises. Maybe one day.

  • Tim Erwin says:

    I’m wondering if async/await can be inferred automatically, so that all code is written synchronously whether or not it was async…Basically, the compiler would infer that it was async based on if a promise was returned. So instead of explicitly writing async/await, those all disappear and the code still functions exactly the same…Just some thoughts…

    Instead of this:

    async function printShippingLabel(personId) {
    const person = await getPerson(personId);
    const address = await getAddress(personId);
    await printLabel(person, address);
    }

    You have…

    function printShippingLabel(personId) {
    const person = getPerson(personId);
    const address = getAddress(personId);
    printLabel(person, address);
    }

    It looks exactly like synchronous code, but the compiler handles the inference and essentially transpiles to async/await in the background…The ultimate syntactic sugar?

  • Comments are closed.