In my last two posts, I showed you how JavaScript Promise
s, 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 Promise
s using new JavaScript language features–if your target environment supports them.
This is the 3rd is a series on JavaScript Promises:
- How They Work
- How They Break
- How They’ll Work Someday
Arrow Functions
If you’ve been working with Promise
s 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 Promise
s.
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 Promise
s, 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:
- How They Work
- How They Break
- How They’ll Work Someday
I still haven’t been able to bring myself to use promises. Maybe one day.
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?