Introduction to JavaScript Async Functions- Promises simplified
Created: March 27th, 17'
JavaScript Promises
provides us with an elegant way to write asynchronous code that avoids
piling on callbacks after callbacks. While the syntax for Promises is
fairly succinct, at the end of the day, it still looks like asynchronous
code, especially as you begin to chain promises together with a series
of then()
:
addthis(5).then((val) => { return addthis(val*2) }).then((val) => { return addthis(val/4) }).then((val) => { return Promise.resolve(val) // return promise that resolves to final number })
Note that I'm using
JavaScript Arrow Functions inside then()
. While the
Promises Pattern is much better than nested callbacks, there is still a
sense of "unnaturalness" and convolution about it with the long chain.
And where there is a need for something better, leave it up to the
JavaScript Gods (or the ECMAScript Body) to provide it, in this case,
Async functions. Part of ECMAscript 6, Async Functions works alongside
Promises to make the later fit in better with the rest of the language,
by upending the later's syntax so it appears synchronous, one line
followed by the next. The result is asynchronous code that's easier to
write, follow, and debug, as it now falls more inline with how most
JavaScript code is written, in a linear, procedural manner. Async
functions are
already supported in the newer versions of Chrome (55) and FF (52),
and IE Edge 15 should follow suit too.
To give you a taste of JavaScript async functions, here's how the above code looks like using an async function instead:
var a = await addthis(5) var b = await addthis(a*2) var c = await addthis(b/4) return c // return promise that resolves to final number
So much cleaner, and well, normal looking! Lets see in detail now how to define and utilize async functions, soon to be your Promises' new best friend.
The Anatomy of an Async Function
Async functions are defined by placing the async
keyword in front of a
function, such as:
async fetchdata(url){ // Do something // Always returns a promise }
This does two things:
- It causes the function to always return a promise whether or
not you explicitly return something, so you can call
then()
on it for example. More on this later. - It allows you to use the
await
keyword inside it to wait on a promise until it is resolved before continuing on to the next line inside the async function.
The await keyword
This brings us to the second part of an async function, which is the
await
keyword. This is where most of the magic of async
functions happen. Using await
, we can hit the pause button and wait for
a function that returns a promise inside an async function to resolve before moving on to
the next line inside the async function. The result is asynchronous code expressed
in a linear, sequential manner that's much easier to follow:
async function fetchdata(){ var text1 = await fetchtext('snippet.txt') //pause and wait for fetchtext() to finish (promise is resolved) var text2 = await fetchtext('snippet2.txt') //Then, pause and wait again for fetchtext() to finish var combined = text1 + text2 return combined // return promise that resolves to return value } fetchdata().then((completetxt) => { console.log(completetxt) // logs combined })
Here we assume function fetchtext()
asynchronously fetches
some text and returns a promise that resolves to its contents. By placing
the await
keyword in front of fetchtext()
when
invoking it, everything else that follows the invocation pauses until
fetchtext()
has resolved. One can think of
await
as having the same effect as calling then()
on an asynchronous function and placing everything else that follows inside
it. When we use await
multiple times, each awaited function
waits for the resolution of the previous before executing.
Recall that a function marked as async always returns a promise. If we
explicitly return a value at the end of our async function (as in line 5
above), the promise will be resolved with that value (otherwise, it resolves
with undefined
). The fact that async functions always returns a promise
makes them non blocking, even though asynchronous operations inside them are
run sequentially. That is why we're able to call then()
after an async
function call in line 8 above, with the explicitly returned value made available via
the parameter.
"Async
functions always returns a promise. If we explicitly return a value at
the end of our async function, the promise will be resolved with that value;
otherwise, it resolves with undefined
."
Just to help you better grasp the underpinnings of the async function pattern, lets see how the above code would look like using Promises only:
// Promise only version function fetchdata(){ return new Promise((resolve, reject) => { var combined = '' fetchtext('snippet.txt').then((text) => { combined = text return fetchtext('snippet.txt2') }).then((text) => { combined += text resolve(combined) }) }) } fetchdata().then((completetxt) => { console.log(completetxt) })
Which version do you - and more importantly - other people looking at
your code - prefer? Most of your Promises based code will inescapably
consist of a bunch of then()
s and multiple return statements that using
async functions you can minimize to make the code much more legible.
Handling errors inside an Async function
So far we haven't talked about dealing with errors inside an Async
function. For example, given the following function, what happens when
the awaited function fetchtext()
doesn't resolve, but instead rejects
its promise?
async function fetchdata(){ var text = await fetchtext('snippet.txt') // what happens if fetchtext() doesn't resolve its promise but rejects? var msg = "The text is " + text return msg } fetchdata().catch((err) => { // fetchdata() returns a rejected promise console.log(err) //msg contains reject('Error Message') })
When an awaited function rejects its promise or throws an error, the error
by default is swallowed silently, and execution of the reminder of
the async function aborted. This means the 2nd and 3rd lines in the
above function would never get run. fetchdata()
when run
will return a promise that's been rejected with the rejected value (if
defined). While we could use the catch()
method of
JavaScript Promises to handle the rejection (as seen above), a more
elegant way is simply to use a try/catch
block inside the
async function itself to handle rejections and errors:
async function fetchdata(){ try{ var text = await fetchtext('snippet.txt') // if fetchtext() rejects, execution jumps to catch() var msg = "The text is " + text return msg } catch(err){ console.log('Something went wrong') return err } } fetchdata().then((msg) => { // fetchdata() returns a fulfilled promise that resolves to undefined or return value of catch block if specified console.log(msg) // msg contains reject('Error Message') })
Using try/catch
inside an async function, we can forgo having
to always call catch()
, but instead handle the error
directly inside where it happened, like with other synchronous code.
The value of err
would simply be the value passed into reject()
inside function fetchtext()
. With a try/catch
block in place to
gracefully deal with rejections inside the async function, running
fetchdata()
will return a promise that's fulfilled (resolved to
undefined
or the return value inside the catch()
block), even though the awaited function inside returns a rejected
promise. Contrast that with when there is no try/catch
block-
fetchdata()
in that case returns a rejected promise itself as
well.
In general it's recommended that you take advantage of try/catch
whenever
defining async functions to deal with errors explicitly and all in one
place, right inside the async function.
Await in parallel versus in sequence
When we have a series of await
functions being called, they are executed
in order, one after the resolution of the previous:
async function fetchdata(){ //await in sequence var text1 = await fetchtext('snippet.txt') var text2 = await fetchtext('snippet2.txt') var text3 = text1 + text2 + " The End." return text3 }
Unless each await
function requires some data from the previous, this
isn't the most optimal arrangement, as it means the amount of time it
takes for fetchdata()
to complete is the sum of all of the awaited
functions' execution times. In situations like these, a better approach
is to run the functions in parallel, which can be done with the help of
Promise.all
:
async function fetchdata(){ //await in parallel var text = await Promise.all([ fetchtext('snippet.txt'), fetchtext('snippet2.txt') ]) var text3 = text.join() + " The End." return text3 }
Promise.all
accepts an array of promises and returns a
"super" promise when all of them have resolved, run in parallel. The
super promise resolves to an array containing the resolved value of each
of the "child" promises. By awaiting on Promise.all
, we
retrieve this array once it's available.
Immediately Invoked Async Functions
Last but not least, async functions like regular (usually anonymous) functions can also be invoked at the same time they're defined, via the IIFE (Immediately Invoked Function Expression) pattern:
(async function(){ await fetchtext('snippet.txt') })();
Using arrow functions:
(async () => { await fetchtext('snippet.txt') })();
We can then call then()
immediately following it to process
any resolved value:
(async () =>{ var text1 = await fetchtext('snippet.txt') var text2 = await fetchtext('snippet2.txt') return text1 + text2 })().then((alltext) => console.log(alltext))
Conclusion
With more and more tasks we perform in JavaScript being asynchronous in nature, any addition to the language that helps us more succinctly and intuitively define those operations is greatly welcomed. As you can see, async functions does not seek to replace or supplant JavaScript Promises - being it is based on Promises itself - but provide a way to express Promises in a much more familiar form that is synchronous. And familiarity is usually a good thing.