Categories:

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.