Handling Async Operations in JavaScript in 2018
JavaScript at its core is a single-threaded, event-driven programming language. In order to remain performant, all “blocking” operations such as network requests and hardware access are performed asynchronously.
What this means is that, instead of your code simply waiting for the operation to complete, you have to register an event listener callback function that gets called when the operation completes.
Promises, Promises
As described in my first article about handling async operations, Promises (introduced in ES2015/ES6) are a great way to handle async callbacks, because they give us a standardised way of chaining actions and handling errors, as shown in the example below:
In the example above we create a function that outputs a User and a Random Quote sequentially. The great thing about this structure is:
- We don’t have any nested callbacks
- The operations happen one after the other
- If an error occurs in one step, all the following steps are automatically skipped and the
catch()
handler is invoked - We return the Promise object, which means a caller of the
getUserQuote()
function can call.then()
and.catch()
on it, to chain other actions, e.g:
getUserQuote()
.then(() => console.log('Profit!!'))
.catch((err) => console.log('No profit for you! :('));
So yeah, Promises are awesome, but theres still a few issues:
- Defining lots of
.then()
and.catch()
functions can still get a bit messy and confusing - Doing Loops with async calls is not very easy — you have to recursively call your function from a
.then()
method.
(Trying to use afor { }
loop will just trigger all the async calls at once, which is fine if thats what you want!)
Enter JavaScript Async Functions!
Async Functions, introduced in ES2017/ES8, make async in JavaScript AWESOME!
Lets take a look at our example above, re-written using an async function:
Yep, that really is all the code needed! :) So whats going on here?
- We define the
getUserQuote()
function as anasync
function. What this means is that the function will always return a promise, which will either resolve successfully, or reject with an error. - Within our async function, we
await
any async operations — an async operation is any other function that returns a Promise. - If an awaited operation throws an error, or its promise is rejected, our
getUserQuote()
function will automatically reject. You can wrap the await statement in a normal try / catch block to catch these errors. - Once an awaited operation succeeds, the returned value from the promise will be stored in the assigned variable, and the async function will continue to the next statement.
You’ll notice we’re not returning a value from our getUserQuote()
function — there is no need to manually return a Promise, however you can use a normal return
statement to either:
- Return a single value, which will be added as “resolved” value of the async function’s promise.
- Return a promise, such as the request() function’s result, i.e.
return request({ url: 'https://randomuser.me/api/'});
Consumers of our getUserQuote()
function can then either await
it themselves, or call .then()
and/or .catch()
to handle the result.
Async Proceedures
Now we can use await
inside functions, it would be cool to use it everywhere!
If you want to use await
at the top-level of your JavaScript program, a neat trick is to use an async “Immediately Executed Function Expression” (IIFE). Sounds complicated, but it isn’t really …
Basically the code
(async () => { /* async code here */ })();
Defines an async
function, and executes it immediately. The advantage of this is that now you don’t need to call .then()
or .catch()
anywhere in your code!
And for my last trick — Async Loops!
The other great feature of async functions, is that we can use await
inside loops!
Using await in the for loop above makes sure that each quote gets printed out in sequence before the final console.log() is called.
The await
keyword works great in both for and do loops, but NOT in Array.forEach()
calls.
Summary
2018 is a great time to be a JavaScript developer — the language and platform is getting better and better. Async functions remove much of the complication that JavaScript developers have previously had to contend with, and with native support for them in NodeJS 8, server side JS development is now awesome too!