Asynchronous programming in JavaScript - Promises, Callbacks and Async-await
Naftali Murgor
Posted on December 10, 2021
Asynchronous programming in JavaScript
Understanding asynchronous programming in JavaScript is fundamental to using JavaScript. Asynchronous means several operations occurring at the same time.
This guide provides and a simple introduction to asynchronous programming in JavaScript. It covers the basics and not everything there is to asynchronous programming in JavaScript.
Fork, clone or download sample project here sample project
Remix the project at glitch.io
JavaScript was initially developed to add interactivity to html elements on a page. For instance, when a page loads, the JavaScript gets loaded then parsed. A button on the page sits there waiting for a click
mouse event. We attach a callback function to the event to be triggered when the click event
fires.
const loginBtn = document.getElementById('login-btn')
loginBtn.addEventListener('click', () => {
// do something when 'click' mouse event fires i.e button // is clicked
})
Asynchronous programming is when two or several operations happen at the same time. In JavaScript, asynchronous programming is highly used and a good grasp is ideal.
Let's say one has a page that displays the coin market cap(Price and volume) of various crypto. You'd asynchronously fetch the data from an API, while the page continues to render during page load. Once the results become available, we render the results on the webpage.
JavaScript offers three ways of performing asynchronous actions:
- Using callbacks
- Using Promises
- Async-await - Most recent development introduced in ES7 version
1. using callbacks
callbacks are functions passed to other functions as values. They are "inline" functions with standard function signature and arguments. They can be arrow functions
or ES5 functions
.
// A simple signature of a callback
const waitUp = (someArgs, callback) => {
setTimeout(() => {
// mimick a delay, e.g fetching data from an api
const fakeData = {
user: 'Kakashi sensei',
age: 27,
village: 'Hidden Leaf',
leadership: '4th Hokage'
}
// pass the data to the callback function argument, we will provide this when we call waitUp later in our program
callback(fakeData) // we then can handle the data we got inside this callback
}, 3000)
}
// consuming the callback and handling the data asyncronously returned by waitUp
waitUp('', (data) => {
console.log(data) // our data is now available for use
})
Callbacks are common in Nodejs, latest versions of Nodejs provide ES6 promises which are more cleaner to use.
2. using promises
Promises are a new standard introduced in the ES6(ES2015)
version. Promises represent proxy values that are yet to resolve.
When consuming a promise, promises exist in three states:
- pending state
- resolved state
- rejected state
While performing operations that do not resolve immediately such as fetching data from a web API or reading file contents from a disk, results from the operation won't immediately be available for use within your program. Promises make it less painful to perform such operations.
// creating a promise, note the new syntax
const waitUp = () =>
return new
Promise((resolve,
reject) => {
// do some operations that won't returns a valu
setTimeout(() => {
// mimick a delay, e.g fetching data from and api
const fakeData = {
user: 'Kakashi sensei',
age: 27,
village: 'Hidden Leaf',
leadership: '4th Hokage'
}
// pass the data to the callback function parameter, we will provide this when we call waitUp later in our program
resolve(fakeData) // we finally resolve with a value once we get the data
}, 3000)
})
// consuming the promise created
waitUp()
.then((data) => {
// do something with the data
})
.catch((err)=> {
// handle the promise rejection
})
Consuming a promise is a matter of chaining the function call with a
.then(() => {})
You may chain as many "dot-thens" to the function call as possible.
However, using promises quicky becomes convoluted and leads to code that is hard to follow as the number of "dot-thens" become hard to follow.
Fetch API uses promises as we shall see. Fetch API provides a cleaner way of making HTTP request from the browser. No more XMLHttpRequest
fetch('http://heroes.glitch.io')
.then((res) => res.json()) // parses the body into JavaScript object literal
.then((data) => console.log(data))
.catch((err) => console.log(err)) // .catch comes last to catch handle any errors when the promise returns an error
In most cases, consuming a promise would be more common especially when making HTTP requests
using a library like axios
and other HTTP tooling and making network calls.
3. async-await
Async-await is a syntactical sugar for promises that was introduced in ES2017
version to make using promises more cleaner. To use async-await:
- Declare a function async by adding the
async
keyword to the function signature.
// an async function
async function waitUp(args) {
}
// in arrow functions
const waitUp = async(args) => {
}
- To perform any asyncronous calls inside the function / expression you declared async,
prepend
await
to the call, like:
async function waitUp() {
const res = await fetch('https://glitch.io/heroes')
const data = await res.json()
// use the data like in a normal function
console.log(data)
}
// to handle promise rejections
async function waitUp() {
try {
const res = await fetch('https://glitch.io/heroes')
const data = await res.json()
// use the data like in a normal function
console.log(data)
} catch(ex) {
// any exceptions thrown are caught here
}
}
handling rejected promise is as easy as wrapping your asyncronous code inside a try { } catch(ex) { } block
Async-await makes handling promise cleaner and less painful. You are more likely to see alot ofasync-await
in modern JavaScript than promises and callbacks.
Promises and async-await are interoperable, this means what can be done using promises can be done using async-await
.
For example:
This implementation becomes:
const waitUp = new Promise((reject, resolve) => {
// do some operations that won't return a value immediately
setTimeout(() => {
// mimick a delay, e.g fetching data from an api
const fakeData = {
user: 'Kakashi sensei',
age: 27,
village: 'Hidden Leaf',
leadership: '4th Hokage'
}
// pass the data to the callback function argument, we will provide this when we call waitUp later in our program
resolve(fakeData) // we finally resolve with a value once we get the data
}, 3000)
})
// consuming the promise we created above
waitUp()
.then((data) => {
// do something with the data
})
.catch((err)=> {
// handle the promise rejection
})
Becomes:
const waitUp = new Promise((reject, resolve) => {
// do some operations that won't returns a valu
setTimeout(() => {
// mimick a delay, e.g fetching data from an api
const fakeData = {
user: 'Kakashi sensei',
age: 27,
village: 'Hidden Leaf'
leadership: '4th Hokage'
}
// pass the data to the resolve callback
resolve(fakeData) // we finally resolve with a value once we get the data
}, 3000)
})
// consuming the promise created using async-await
// assuming a main function somewhere:
const main = async() => {
const data = await WaitUp()
// use the data like in a syncronous function call
console.log(data)
}
main() // calling main
Summary
Understanding the asynchronous aspect of JavaScript is crucial. Constant practise and using promises in a project helps solidify understanding usage of Promises.
Async-await does not replace promises but makes code cleaner and easy to follow. No more .then(fn)
chains
Follow me on twitter @nkmurgor where I tweet about interesting topics.
Do you feel stuck with learning modern JavaScript? You may preorder Modern JavaScript Primer for Beginners where I explain everything in a clear and straight-forward fashion with code examples and project examples.
This article was orignally published at naftalimurgor.com
Thanks for stopping by!
Posted on December 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.