DEV Community

Cover image for How To Rock 🀟 Asynchronous Calls By Understanding JavaScript Callbacks, ES6 Promises And ES7 Async/Await πŸ”₯😎
Marc Backes
Marc Backes

Posted on • Originally published at developer.blog

How To Rock 🀟 Asynchronous Calls By Understanding JavaScript Callbacks, ES6 Promises And ES7 Async/Await πŸ”₯😎

Cross-posted from developer.blog

Callbacks can be incredible useful things when programming in JavaScript, however the coding can get messy when using them a lot. This post explains how promises and async/await from modern JavaScript specifications work and how they improve readability in your code.

In this post I'll use arrow functions, which you can read up on the first chapter of my arrow function blog post.

Callbacks

One of the most brilliant things in JavaScript is that functions are seen as objects. This makes it possible to pass functions as parameters to another function which can then call the passed function within. The passed function is called a callback function.

This comes in handy when processing tasks that sun asynchronous and we cannot be certain when exactly the task has finished, so we can process the resulted data. A real world example for this is requesting data from a REST API.

Here is an example with traditional callbacks of a function that -for demonstration purposes- needs 2 seconds to add two numbers:

// Definition of the asynchronous function
const add = (a, b, callback) => {
    setTimeout(() => {
        const result = a + b
        callback(result)
    }, 2000);
}

// Calling the asynchronous function and passing the callback function
add(3, 6, sum => {
    // Execute this when result is ready
    console.log(`The sum is: ${sum}`)
})
Enter fullscreen mode Exit fullscreen mode

When you execute that code, the add function is being called and after two seconds, the callback function will be executed with the result (logged to the console).

Doesn't look that bad, right? But there are two things that make this aproach tiresome to use:

  • When trying to introduce error handling (something went wrong)
  • When trying to use various callback functions after each other

Error handling

Let's assume our ficticious function is only able to add positive numbers. We would want the user to know there was a problem when trying to process negative numbers.

const add = (a, b, callback) => {
    setTimeout(() => {
        // Checking if the input numbers are right
        if(a >= 0 && b >= 0) {
            const result = a + b
            callback(result)
        } else {
            // Passing an error if there is a negative input
            callback(undefined, 'Numbers must be non-negative')
        }
    }, 2000);
}

add(3, -6, (sum, error) => {
    // If an error occured in the add function, display it
    if(error) {
        console.log(`An error occured: ${error}`)
    } else {
        console.log(`The sum is: ${sum}`)
    }
})
Enter fullscreen mode Exit fullscreen mode

Chaining

Executing various callbacks after each other (chaining), or otherwise known as "callback hell" can get really messy really fast.

Let's say we want to calculate the square of the resulting sum, and afterwards check if that square is an odd or even number. Each taking 1 fake additional second to execute.

const add = (a, b, callback) => {
    setTimeout(() => {
        // Checking if the input numbers are right
        if(a >= 0 && b >= 0) {
            callback(a + b)
        } else {
            // Passing an error if there is a negative input
            callback(undefined, 'Numbers must be non-negative')
        }
    }, 2000);
}

const tripleDown = (a, callback) => {
    setTimeout(() => {
        callback(a * 3)
    }, 1000);
}

const isEven = (a, callback) => {
    setTimeout(() => {
        callback(a % 2 === 0)
    }, 1000);
}

add(3, -6, (sum, error) => {
    // If an error occured in the add function, display it
    if(error) {
        console.log(`An error occured: ${error}`)
    } else {
        square(sum, tripleResult => {
            isEven(square, isEvenResult => {
                console.log(`The sum is: ${sum}`)
                console.log(`The triple of the sum is: ${tripleResult}`)
                console.log(`The triple is even: ${isEvenResult}`)
            })
        })
    }
})
Enter fullscreen mode Exit fullscreen mode

I think we can now agree that the code starts getting messy which makes it difficult to understand and maintain after a while.

Promises

Promises to the rescue! In 2015, when ES6 was released, a nifty little feature was introduced which made it possible for developers to escape the callback hell.

A promise is exactly what the name suggests it is: It is a promise that there will be a result at some time in the future. That result can be successful, then the promise would be fullfilled or it could have failed, which would make the promise rejected. While there is no answer (yet), the promise is pending.

Let's write the code we had at the beginning (example of adding two numbers with a two second delay) with a promise.

const add = (a, b) => {
    // Returning a promise that there will be an answer sometime
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // Resolving the promise
            // This means it was successful
            resolve(a + b)
        }, 2000);
    })
}

// Executing the add function, *then* executing the callback.
add(2, 9).then(sum => {
    console.log(`The sum is: ${sum}`)
})
Enter fullscreen mode Exit fullscreen mode

When the promise we created is being resolved, .then() is being executed and it will have whatever value has been passed in the resolve call.

Error handling

Handling errors is a delight with promises. Instead of having the callback function to accept an extra parameter.

Instead of calling resolve() in the promise, we have to call reject() for the promise to end unsuccessfully. Let's extend the example with adding the resitrictions of not processing negative numbers:

const add = (a, b) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(a >= 0 && b >= b) {
                // The promise is being fullfilled successully
                resolve(a + b)
            } else {
                // The promise is being fullfilled unsuccessully
                reject('Numbers must be non-negative')
            }
        }, 2000);
    })
}
Enter fullscreen mode Exit fullscreen mode

Handling that error is quite elegant now. We just add a .catch() to the promise execution:

add(2, -9).then(sum => {
    // Processing the asynchonous function result
    console.log(`The sum is: ${sum}`)
}).catch(error => {
    // The error has being "caught"
    console.log(`An error occured: ${error}`)
})
Enter fullscreen mode Exit fullscreen mode

Chaining

To chain various asynchronous functions together is also a bit easier now. Here an example on chaining three times the same add() function. First adding 2+5, then the result of that + 43, then the result of that + 1000.

add(2, 5).then(firstSum => {
    console.log('first sum', firstSum);
    return add(firstSum, 43)
}).then(secondSum => {
    console.log('second sum', secondSum);
    return add(secondSum, 1000)
}).then(thirdSum => {
    console.log('third sum', thirdSum);
}).catch(error => {
    console.log('error', error);
})
Enter fullscreen mode Exit fullscreen mode

This is way cleaner and people were really excited about this back in 2015, because they could finally deliver cleaner code and kick their callback hells back where they came from (regular hell).

There were still two problems though:

  • In the callback of each callback, you don't have access to the results inbetween (e.g. you can't access firstSum on the third .then()
  • It still is not that intuitive to chain asynchronous functions together

These two problems were solved in ES7 which was released a year later.

Async/Await

Async/Await is not a new technology, rather than a new toolset that's built on top of promises. It is designed to make asynchronous functions really easy to code and later on understand, with a syntax that flows off the keyboard pretty naturally. The great thing is, that something that's already programmed with promises will continue to work with async/await, because we just write code in a different manner rather than a new technology.

async

When you put the async keyword in front of a function (doesn't matter if arrow or regular), it automatically returns a (resolved) promise rather than the value returned.

const doAsynchronousStuff = async () => {
    return 4711;
}

// Returns: Promise { 4711 }
Enter fullscreen mode Exit fullscreen mode

await

When using the await in front of a function call, JavaScript waits for the promise to be fullfilled before continuing with the next line of execution.

await can only be used inside a async function!

Let's check out this example (assuming the add function from Promises > Error handling already exists:

const doCalculations = async () => {
    const sum = await add(13, 99)
    return sum
}

doCalculations().then(result => {
    console.log(`The result is: {result}`)
})
Enter fullscreen mode Exit fullscreen mode

Error handling

The next line after an await function call is only being executed when the promise has been fullfilled. When it's being rejected, all future execution in the asynchonous function is being stopped.

There is a way though to catch errors for each individual awaitfunction call, using a good old-fashioned try/catch statement:

const doCalculations = async () => {
    let sum;
    try {
        // Try to execute this...
        sum = await add(13, -99)
    } catch (error) {
        // If something goes wrong, we catch the error here
        console.log(`An error occured: ${error}`);
    }
    return sum
}
Enter fullscreen mode Exit fullscreen mode

Chaining

Chaining now is even easier than before. The way you write the code lets you even believe that they are synchronous calls, but in reality, all the Promise magic happens behind the scenes.

const doCalculations = async () => {
const sum = await add(13, -99)
const sum2 = await add(sum, 1000)
const sum3 = await add(sum2, 9999)

// You could access all three variables here.
// For example to do comparisons

return sum3
Enter fullscreen mode Exit fullscreen mode

}

Summary πŸ™Œ

async/await is an industry standard now and it is recommendable you use it, as it gives you many advantages. It is however important to know where it comes from and how it works under the hood. When using it, it's easy to forget we're actually doing asynchronous calls.

Now you should be all set to create your own libraries with Promise support and use existing libraries that do support promises already (all of the important ones do) in an easy and readable way.

Photo by Alex on Unsplash

Top comments (4)

Collapse
 
themarcba profile image
Marc Backes

What I meant with that is that it’s recommendable for code readability. I code on Node.js, so there it doesn’t matter. And for client-side JS you can always transpile your code, so even older browsers support your app

Collapse
 
luisnomad profile image
Luis Serrano πŸ‡ͺπŸ‡Ί

Thanks for this! When I first approached this topic it seemed obvious to everyone but I struggled a bit, specially with async and loops.

Collapse
 
themarcba profile image
Marc Backes

You’re welcome 😊 Glad I could be of service

Collapse
 
themarcba profile image
Marc Backes

But your point is great. I’ll update the blog post to clarify that for users πŸ‘ Thanks for your input