ES2017 async/await to the rescue!
TL;DR TL;DR: Do yourself a favor — try using async/await and indent less! Setup is easy enough.
TL;DR: ES2017 async/await is THE solution for “callback hell” (ie. async flow control and error handling). Soon you’ll be able to use it without transpilation in modern browsers. In the meantime either transpile or use ES2015 generator functions along with a small lib (eg. co). async/await is basically syntax sugar over ES2015 promises and generators.
A story of your frustration? So you’re a JS developer, you’ve probably coded some ajax in your career, maybe you even had a little afair with Node.js and did some async IO. If you went anything past a very simple app, you probably experienced some frustration with omnipresent callbacks marching into your code and destroying the landscape. You thought, yes, it’s JavaScript, it’s async and that’s just the way it is. But it just didn’t feel right. Your code felt cumbersome and brittle. You started looking for some solutions, you probably heard of promises/futures (or deferreds, as some similar construct was once called in jQuery). Maybe you tried a library like async or one of many others available on npm. It solved the problem to some extent, but not really. After all, the callbacks were still there, the solutions had their own quirks and it didn’t look clean enough. You might even have thought: “JS is hopeless in this area of synchronizing IO operations. It’s time to move out! How can this thing be so inherently difficult in JS, while in other languages it is so easy”.
You probably heard something of mysterious async/await keywords supposed to be introduced in some future version of JS, but it somehow got lost amidst all the other new shiny JS stuff being introduced all the time at a pace no one can keep up with. You thought, it’s just another guy’s solution to a hopeless problem. So here’s the good news: async/await is really the best of both worlds (sync and async IO). Benefits of one without sacrificing the other. Code doesn’t spill right anymore, it almost looks like classical sync IO. Just try it, you’ll never wanna go back. Have a look at a code sample:
async function readAndProcess( path ) { let content = await fs.readFile( path, "utf8" )
let result = await processAsync( content ) return result
}
This way you can use all the “traditional” (synchronous) JS flow control and error handling mechanisms on async operations. Ever heard of if/else? :) Good! How about try/catch? No need to write libraries to make simple logical operations. Error propagation for free, who’d think (something other languages have for ages :)). But hey, don’t forget this is an async world, where you have an event loop for free (in other languages, in turn, you’d need to resort to a library, like eventmachine in ruby).
Another good news, it’s already implemented or being implemented in major browsers. V8 is almost there, meaning soon you’ll be able to use it in Node without transpilation. For the time being you can either achieve it via babel transpilation — here’s a gist describing how to do it in node without transpiling all the other ES6 stuff (which is already implemented in newest versions of node). You can also try async-to-gen, which aims to transpile it with minimal overhead. In node async/await can be transpiled to ES6 generators, which are already natively available. For browser stuff you’ll probably still want to transpile with babel to plain ES5. Anyway, soon it should be available in all major modern browsers and in node without transpilation!
Yet another good news is, if transpilation is a no-go for you, you can still achieve almost the same effect without transpilation in environments which already have ES6 generators implemented (in particular, node). Generator functions are capable of doing this with some help of a little library, like co, which even humbly describes itself as a stepping stone towards async/await. There are many other libs, and basically it doesn’t matter which one you’ll use, as hopefully it’s only a temporary solution. Resulting code is very similar to target async/await code (one could say it’s “1 to 1”). Maybe it’s a little simplification, but for most cases rewriting to async/await should be more or less converting co function calls to async and yield keywords to await. Have in mind the eventual solution if you decide for this path. Below some sample code with co + generator functions. Just one level of indentation more in comparison with async/await, no disaster!
function readAndProcess( path ) {
return co( function * () { let content = yield fs.readFile( path, "utf8" )
let result = yield processAsync( content ) return result
} )
}
PS, wonder how fs.readFile can return promises? Just use a module like mz or many others.
The above is possible because async/await keywords are just a syntactic sugar over ES6 generator functions. It’s them that provide the real functionality, which basically is to allow a JS function to pause and resume later - in particular, after an async operation is finished (a point easy to overlook, effectivelly leading to dismissing the idea of generator functions as nothing fancy). It’s funny, because to me generators didn’t at all look related to async flow control, when I was first browsing through ES6 features.
Another important point is that promises are not going anywhere! I’m assuming you’re aware that they made it to ES6 spec and are already natively available in eg. v8. Actually, if you like promises, you’ll be happy to hear that promises, along with generators, are the foundation of async/await solution. Basically, in the co solution, you just pass promises with yield keyword back to the caller. I recommend this series of posts to learn more about generator functions. You’ll basically know how to build a library like co yourself.
Getting back to promises, not only are they the foundation of async/await, but you’ll still need to resort to them when implementing some more complex async IO scenarios (like simply running a couple operations in parallel, achieved eg. with Promise.all). Basically, await keyword expects a promise and doesn’t proceed to the next line of code until it resolves (or fails). The keyword async, in turn, makes the function always return a promise, and allows using await in its body. The bottom line is: there’s no way around promises, you better get familiar with them, it’ll help you.
I’m just wondering whether it will be eventually possible to use await in top-level scope. You could then, for example, easily launch some IO operation in the console and get the result. Currently I often end up doing:
someTask.then(res => console.log(res), err => console.log(err))
Ok, that’s it, thanks for getting this far, I hope you benefitted from this read. It’s nothing really revealing, I just wanted to contribute to the spreading of the idea and share some enthusiasm :)
Here are some things I’d like to improve in this blog post:
- more code samples
- error handling with async/await or generators
- generator folding