The way JavaScript ES8 made me to STOP writing call back function.

Muthukumaraswamy
8 min readSep 18, 2017

--

It is fun with doing modern development with Javascript . when I look back I would have messed up the asynchronous tasks, either too long or too complex. JavaScript now provides a new syntax for handling these operations, and it can turn even the most convoluted asynchronous operations into concise and highly readable code.

Asynchronous JavaScript And XML

In the early-to-mid 1990s, Ajax was the first major historical breakthrough in asynchronous Javascript. The term Ajax has come to represent a broad group of Web technologies that can be used to implement a Web application that communicates with a server in the background, without interfering with the current state of the page.This technique allowed websites to pull and display new data after the HTML had been loaded, a revolutionary idea at a time when most websites would download the entire page again to display a content update. The technique (popularized in name by the bundled helper function in jQuery) dominated web-development for all of the 2000s, and Ajax is the primary technique that websites use to retrieve data today, but with XML largely substituted for JSON.

NodeJS

JavaScript — arguably the most ubiquitous client-side technology — has cast a wide net over front-end development. But it wasn’t until the creation of the Node.js platform in 2009 that JavaScript became a player in back-end technology — migrating asynchronous computing over to the server side and bringing all its benefits with it.

Promises was another major improvement, but they still can often be the cause of somewhat verbose and difficult-to-read blocks of code.

Now there is a solution.

Async/await is a new syntax (borrowed from .NET and C#) that allows us to compose Promises as though they were just normal synchronous functions without callbacks. It’s a fantastic addition to the Javascript language, added last year in Javascript ES7, and can be used to simplify pretty much any existing JS application.

EXAMPLES

Let us go through a few code examples.

Async/await is fully supported in the latest versions of Chrome, Firefox, Safari, and Edge, so you can try out the examples in your browser console. Additionally, async/await syntax works in Nodejs version 7.6 and higher, and is supported by the Babel and Typescript transpilers, so it can really be used in any Javascript project today. No libraries are required to run these examples.

If you want to follow along on your machine, we’ll be using this dummy API class. The class simulates network calls by returning promises which will resolve with simple data 100ms after being called.

class Api {
constructor () {
this.user = { id: 1, name: 'test' }
this.friends = [ this.user, this.user, this.user ]
this.photo = 'This is not real'
}

getUser () {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.user), 100)
})
}

getFriends (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.friends.slice()), 100)
})
}

getPhoto (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.photo), 100)
})
}

throwError () {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Intentional Error')), 100)
})
}
}

Each example will be performing the same three operations in sequence: retrieve a user, retrieve their friends, retrieve their picture. At the end, we will log all three results to the console.

Nested Promise Callback Functions

An example of implementation using nested promise callback functions.

function callbackpain () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.getPhoto(user.id).then(function (photo) {
console.log('callbackpain', { user, friends, photo })
})
})
})
}

The code block, which has a reasonably simple purpose, is long, deeply nested, and ends in this…

})
})
})
}

In a real codebase, each callback function might be quite long, which can result in huge and deeply indented functions. Dealing with this type of code, working with callbacks within callbacks within callbacks, is what is commonly referred to as “callback pain”.

Even worse, there’s no error checking, so any of the callbacks could fail silently as an unhandled promise rejection.

Promise Chain

Let’s make the code better.

function promiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('The promise Chain', { user, friends, photo })
})
}

One nice feature of promises is that they can be chained by returning another promise inside each callback. This way we can keep all of the callbacks on the same indentation level. We’re also using arrow functions to abbreviate the callback function declarations.

This variant is certainly easier to read than the previous, and has a better sense of sequentiality, but is still very verbose and a bit complex looking.

Async/Await

is it Possible without any callback functions? Impossible?

async function asyncAwaitIsYourNewBestFriend () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}

Much better. Calling “await” in front of a promise pauses the flow of the function until the promise has resolved, and assigns the result to the variable to the left of the equal sign. This way we can program an asynchronous operation flow as though it were a normal synchronous series of commands.

I am excited on this .

Note that “async” is declared at the beginning of the function declaration. This is required and actually turns the entire function into a promise. We’ll dig into that later on.

LOOPS

Async/await makes lots of previously complex operations really easy.

Recursive Promise

Fetching each friend list sequentially might look with normal promises

function promiseLoops () {  
const api = new Api()
api.getUser()
.then((user) => {
return api.getFriends(user.id)
})
.then((returnedFriends) => {
const getFriendsOfFriends = (friends) => {
if (friends.length > 0) {
let friend = friends.pop()
return api.getFriends(friend.id)
.then((moreFriends) => {
console.log('promiseLoops', moreFriends)
return getFriendsOfFriends(friends)
})
}
}
return getFriendsOfFriends(returnedFriends)
})
}

We're creating an inner-function that recursively chains promises for the fetching friends-of-friends until the list is empty. Ugh. It’s completely functional, which is nice, but this is still an exceptionally complicated solution for a fairly straightforward task.

Note — Attempting to simplify the promiseLoops() function using Promise.all() will result in a function that behaves in significantly different manner. The intention of this example is to run the operations sequentially (one at a time), whereas Promise.all() is used for running asynchronous operations concurrently (all at once). Promise.all() is still very powerful when combined with async/await, however, as we'll see in the next section.

Async/Await For-Loop

Much easier

async function asyncAwaitLoops () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)

for (let friend of friends) {
let moreFriends = await api.getFriends(friend.id)
console.log('asyncAwaitLoops', moreFriends)
}
}
Just a for-loop. Async/await is your friend.
eah, of course we can. It solves all of our problems.
async function asyncAwaitLoopsParallel () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const friendPromises = friends.map(friend => api.getFriends(friend.id))
const moreFriends = await Promise.all(friendPromises)
console.log('asyncAwaitLoopsParallel', moreFriends)
}

To run operations in parallel, form an array of promises to be run, and pass it as the parameter to Promise.all(). This returns a single promise for us to await, which will resolve once all of the operations have completed.

ERROR HANDLING

There is, however, one major issue in asynchronous programming that we haven’t addressed yet: error handling. The bane of many codebases, asynchronous error handling often involves writing individual error handling callbacks for each operation. Percolating errors to the top of the call stack can be complicated, and normally requires explicitly checking if an error was thrown at the beginning of every callback. This approach is tedious, verbose and error-prone. Furthermore, any exception thrown in a promise will fail silently if not properly caught, leading to “invisible errors” in codebases with incomplete error checking.

Let’s go back through the examples and add error handling to each. To test the error handling, we’ll be calling an additional function, “api.throwError()”, before retrieving the user photo.

Attempt 1 — Promise Error Callbacks

Let’s look at a worst-case scenario.

function callbackErrorHell () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.throwError().then(function () {
console.log('Error was not thrown')
api.getPhoto(user.id).then(function (photo) {
console.log('callbackErrorHell', { user, friends, photo })
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}

This is just awful. Besides being really long and ugly, the control flow is very unintuitive to follow since it flows from the outside in, instead of from top to bottom like normal, readable code. Awful. Let’s move on.

Attempt 2 — Promise Chain “Catch” Method

We can improve things a bit by using a combined Promise “catch” method.

function callbackErrorPromiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.throwError()
})
.then(() => {
console.log('Error was not thrown')
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('callbackErrorPromiseChain', { user, friends, photo })
})
.catch((err) => {
console.error(err)
})
}

This is certainly better; by leveraging a single catch function at the end of the promise chain, we can provide a single error handler for all of the operations. However, it’s still a bit complex, and we are still forced to handle the asynchronous errors using a special callback instead of handling them the same way we would normal Javascript errors.

Attempt 3 — Normal Try/Catch Block

We can do better.

async function aysncAwaitTryCatch () {
try {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
await api.throwError()
console.log('Error was not thrown')
const photo = await api.getPhoto(user.id)
console.log('async/await', { user, friends, photo })
} catch (err) {
console.error(err)
}
}

Here, we’ve wrapped the entire operation within a normal try/catch block. This way, we can throw and catch errors from synchronous code and asynchronous code in the exact same way. Much simpler.

COMPOSITION

I mentioned earlier that any function tagged with “async” actually returns a promise. This allows us to really easily compose asynchronous control flows.

For instance, we can reconfigure the earlier example to return the user data instead of logging it. Then we can retrieve the data by calling the async function as a promise.

async function getUserInfo () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
return { user, friends, photo }
}
function promiseUserInfo () {
getUserInfo().then(({ user, friends, photo }) => {
console.log('promiseUserInfo', { user, friends, photo })
})
}

Even better, we can use async/await syntax in the receiver function too, leading to a completely obvious, even trivial, block of asynchronous programing.

async function awaitUserInfo () {
const { user, friends, photo } = await getUserInfo()
console.log('awaitUserInfo', { user, friends, photo })
}

What if now we need to retrieve all of the data for the first 10 users?

async function getLotsOfUserData () {
const users = []
while (users.length < 10) {
users.push(await getUserInfo())
}
console.log('getLotsOfUserData', users)
}

How about in parallel? And with airtight error handling?

async function getLotsOfUserDataFaster () {
try {
const userPromises = Array(10).fill(getUserInfo())
const users = await Promise.all(userPromises)
console.log('getLotsOfUserDataFaster', users)
} catch (err) {
console.error(err)
}
}

CONCLUSION

With the rise of single-page javascript web apps and the widening adoption of NodeJS, handling concurrency gracefully is more important than ever for Javascript developers. Async/await alleviates many of the bug-inducing control-flow issues that have plagued Javascript codebases for decades and is pretty much guaranteed to make any async code block significantly shorter, simpler, and more self-evident. With near-universal support in mainstream browsers and NodeJS, this is the perfect time to integrate these techniques into your own coding practices and projects.

--

--