If you are only getting started with programming, chances are, you are thinking about programs as a set of sequential blocks of logic, where each block does a specific thing and passes its result so the next block can run and so on, and for the most part you are right, most programs run in a sequential manner, this model allows us to build programs that are simple to write and maintain. There are specific use cases however where this sequential model wouldn't work, or wouldn't be optimal. As an example, consider a book reader application. This application has a few advanced features such as finding all occurrences of a word, navigating between bookmarks, and similar. Now imagine the user is currently reading a long book and decides to look for all the occurrences of a common word such as “The”. The application will normally take a couple of seconds to find and index all the occurrences of that word. In a sequential program, the user cannot interact with the application (change the page or highlight a text) until the search operation is fulfilled. Hopefully, you can see that that’s not an optimal user experience!
The diagram illustrates a typical execution flow of the book reader application. If the user initiates a long-running operation (in this case the search for all occurrences of “the” in a large book), the application “freezes” for all the duration of that operation. In this case, the user will keep clicking on the next bookmark button with no result until the search operation is finished and all the operations will take effect at once giving the end user the feel of a lagging application.
You might have noticed that this example doesn't really correspond to the sequential model we introduced earlier. This is because the operations here are independent of each other. The user doesn’t need to know about the number of occurrences of “the” in order to navigate to the next bookmark, so the order of execution of operations is not really important. We don’t have to wait for the end of the search operation before we can navigate to the next bookmark. A possible improvement to the previous execution flow is based on this logic: we can run the long search operation in the background, proceed with any incoming operations, and once the long operation is done, we can simply notify the user. The execution flow becomes as follows:
With this execution flow, the user experience is significantly improved. Now the user can initiate a long-running operation, proceed to use the application normally, and get notified once the operation is done. This is the basis of asynchronous programming.
Javascript, amongst other languages, supports this style of asynchronous programming by providing extensive APIs to achieve just about any asynchronous behavior you can think of. At the end of the day, Javascript should be inherently an asynchronous language. If we refer to the previous example, the asynchronous logic is at the base of all user-interaction applications, and Javascript was primarily built to be used on the browser where most of the programs are about responding to user actions.
The following will give you a brief guide on Asynchronous Javascript:
Callbacks
In a typical program, you will usually find a number of functions. To use a function, we call it with a set of parameters. The function code will execute and return a result, nothing out of the ordinary. Asynchronous programming shifts this logic slightly. Going back to the example of the book reader application, we cannot use a regular function to implement the search operation logic since the operation takes an unknown amount of time. A regular function will basically return before the operation is done, and this is not the behavior we are expecting. The solution is to specify another function that will be executed once the search operation is done. This models our use case as our program can continue its flow normally and once the search operation is finished, the specified function will execute to notify the user of the search results. This function is what we call a callback function:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
First, we define the search operation function, searchOccurrences. It takes the word to search for and a second parameter “callback” which will be the function to execute once the search operation is done. The search operation function was intentionally kept abstract, we only need to focus on its two possible outcomes: the first case is where everything went successful and we have the result of the search in the result variable. In this case, we just have to call the callback function with the following parameters: the first parameter is null meaning that no error has occurred, the second parameter is the word that was searched, and the third and perhaps most important parameter of the three, is the result of the search operation.
The second case is where an error occurs, this is also a case where the execution of the search operation is done and we have to call the callback function. We use a try and catch block to intercept any error and we just call the callback function with the error object from the catch block.
We then defined the callback function, handleSearchOccurrences, we kept its logic quite simple. It is just a matter of printing a message to the console. We first check the “err” parameter to see if any error occurred in the main function. In that case, we just let the user know that the search operation ended with an error. If no errors were raised, we print a message with the result of the search operation.
Finally, we call the searchOccurrences function with the word “the”. The function will now run normally without blocking the main program and once the search is done, the callback will be executed and we will get the result message either with the search result or the error message.
It’s important to mention here that we only have access to the result variable inside the main and the callback functions. If we try something like this:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
the result of the print would be undefined because the programs do not wait for the searchOccurrences function to execute. It moves to the next instruction which is the print statement before the result variable is assigned inside the main function. As a result, we will print the unassigned result variable.
So based on this logic, we should keep all the code that uses the result variable inside the callback function. This might not seem a problem now but it could quickly escalate into a real problem. Imagine the case where we have a chain of asynchronous functions that need to run in sequence, In the typical callback logic, you would implement something like this:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Keep in mind that each callback has an error parameter and each error has to be handled separately. This makes the already complex code above even more complex and hard to maintain. Hopefully, Promises are here to solve the callback hell problem, we will cover that next.
Promises
Promises are built on top of callbacks and operate in a similar fashion. They were introduced as part of ES6 features to solve a few of the glaring issues with callbacks such as callback hell. Promises provide their own functions that run on successful completion (resolve), and when errors occur (reject). The following showcases the searchOccurrences example implemented with promises:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Let’s go over the changes we applied:
The searchOccurrences function returns a promise. Inside the promise, we keep the same logic: we have two functions resolve and reject that represent our callbacks rather than having a single callback function that handles both a successful execution and an execution with errors. Promises separate the two outcomes and provide a clean syntax when calling the main function. The resolve function is “hooked” to the main function using the “then” keyword. Here we just specify the two parameters of the resolve function and print the search result. A similar thing applies to the reject function, it can be hooked using the “catch” keyword. Hopefully, you can appreciate the advantages promises offer in terms of code readability and cleanliness. If you are still debating it, check out how we can solve the callback hell problem by chaining together the asynchronous functions to run one after the other:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Async/Await are the latest addition to our asynchronous toolbelt in Javascript. Introduced with ES8, they provide a new layer of abstraction on top of asynchronous functions by simply “waiting” for the execution of an asynchronous operation. The flow of the program blocks at that instruction until a result is returned from the asynchronous operation and then the program will proceed with the next instruction. If you are thinking about the synchronous execution flow you are correct. We have come full circle! Async/await attempts to bring the simplicity of synchronous programming to the asynchronous world. Please keep in mind that this is only perceived in the execution and the code of the program. Everything stays the same under the hood, Async/await are still using promises and callbacks are their building blocks.
Let’s go over our example and implement it using Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Our code didn't change much, the important thing to notice here is the “async” keyword before the searchOccurrences function declaration. This indicates that the function is asynchronous. Also, notice the “await” keyword when calling the searchOccurrences function. This will instruct the program to wait for the execution of the function until the result is returned before the program can move to the next instruction, in other words, the result variable will always hold the returned value of the searchOccurrences function and not the promise of the function, in that sense, Async/Await doesn't have a pending state as Promises. Once the execution is done, we move to the print statement and this time the result actually contains the result of the search operation. As expected, the new code has the same behavior as if it were synchronous.
Another minor thing to keep in mind is that since we no longer have callback functions, we need to handle the searchOccurrences error inside the same function since we cannot just propagate the error to the callback function and handle it there. Here we are just printing an error message in case of an error for the sake of the example.
Wrap-up
In this article we went through the different approaches used to implement asynchronous logic in Javascript. We started by exploring a concrete example of why we would need to shift from the regular synchronous style of programming to the asynchronous model. We then moved to callbacks, which are the main building blocks of asynchronous Javascript. The limitations of callbacks led us to the different alternatives that were added over the years to overcome these limitations, mainly promises and Async/await. Asynchronous logic can be found anywhere on the web, whether you are calling an external API, initiating a database query, writing to the local filesystem, or even waiting for user input on a login form. Hopefully, you now feel more confident to tackle these issues by writing clean and maintainable Asynchronous Javascript!
If you like this article, please check out the CLA Blog where we discuss various subjects on how to get into tech. Also, check out our youtube channel for our previous free workshops and follow us on social media so you don’t miss out on upcoming ones!