Asynchronous JavaScript: Organizing Callbacks for Readability and Reusability
Time to read: 8 minutes
Asynchronous programming is an essential part of being a complete JavaScript programmer. Understanding the code execution flow in JavaScript is the foundation of understanding how it can handle asynchronous tasks. And being able to program asynchronous tasks enables you to take advantage of the extensive array of functions provided by JavaScript runtime engines and external APIs. With those tools you can connect your JavaScript programs with web APIs across the internet and effectively manage those—sometimes tenuous—connections.
With power comes complexity. Using JavaScript callbacks to implement asynchronous functionality can quickly create a series of deeply nested function calls. This post will show you how to write and organize your JavaScript callbacks to maximize the readability, maintainability, and reusability of your asynchronous functions.
As an old programming saying goes:
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
The first part of this series, Asynchronous JavaScript: Understanding Callbacks, provides a complete overview of the JavaScript event loop, callback queue, and execution stack. In a short case study project it shows you how the order of function execution works in JavaScripts non-blocking event model and how callbacks can be used to retrieve and manipulate data from external APIs.
This post picks up where the first part left off and show you how to convert a series of three linked callbacks and functions that's five levels deep into a more maintainable and debuggable code. Getting set up with the case study project will take just a few minutes.
Prerequisites
To accomplish the tasks in this post you will need the following:
- Node.js and npm (The Node.js installation will also install npm.)
- Git
You should also have a well-grounded understanding of JavaScript and the JavaScript event loop. If you need a refresher on the latter point, check out the first part of this series on Asynchronous JavaScript.
There is a companion repository for this post available on GitHub.
Initializing the project
If you started this series from the beginning, great! You already have the project and code setup. You can continue with the asynchronous-javascript Node.js application.
If you already know how the event loop works and are here to focus on code organization there are two ways to get started:
Option 1
Clone the project from Part 1 by executing the following instructions in the directory where you'd like to create the project directory:
Option 2
Execute the following command-line instructions in the directory where you'd like to create the project directory:
After successfully completing option 1 or 2 you should have a Node.js project with a nestedCallbacks.js file in the project root directory. There's also a Git repo with an appropriate .gitignore file for a Node.js project.
If you followed Option 1, open the nestedCallbacks.js file in the root project directory.
If you followed Option 2, create a nestedCallbacks.js file in the root project directory and insert the following JavaScript code:
Verify your Node.js project is set up correctly by executing the following command-line instruction in the root project directory:
The output displayed in your console window should be:
You may not agree with the result, but it demonstrates your code is working correctly and your API calls are connecting with the remote API mockup.
Understanding the problem with nested JavaScript callbacks
Before starting in on reorganizing your callbacks it will be helpful to have an understanding of the problem by taking a look at a series of nested callbacks.
Look at the nestedCallbacks.js code in your IDE or code editor. If you're using Visual Studio Code (and why wouldn't you?) you'll see that the forEach
loop in the callback function for the third API call is at the sixth level of nesting.
While the code in the first two callbacks is simple and the third callback function is straightforward, it's not hard to see how finding your place in the callback queue could become complicated. Imagine if there were choices among API calls, callback functions, and arguments depending on conditional logic depending on the return values of the API calls. It could be a recipe for spaghetti!
Organizing your asynchronous JavaScript callbacks
In addition to being potentially difficult to read, maintain, and debug as the program grows in complexity, the API calls and callback functions in nestedCallbacks.js can't be reused in their current form. That means you're likely to have duplicate code, leading to more opportunities for bugs.
Fortunately, it's easy to refactor nestedCallbacks.js into more modular and reusable code. One technique is to start from the innermost callback and work your way up.
So you can compare the two versions side-by-side as you work, create an organizedCallbacks.js file in the project root directory.
Insert the following JavaScript code:
Encapsulate the most nested code block, which is the callback function for the last request
call. Insert the following code below the line you just added:
You may have noticed that there are more parameters in this function. Here's what they do:
movie
is an object containing information about the movie for which you are going to calculate the average score.
reviews
is an array of reviews of the specified movie
. Each array element has a review score.
director
is an object containing information about the director of the specified movie
.
toBeChecked
is a counter of how many movies remain to be processed by the function.
Note that toBeChecked
is an object instead of a primitive (integer) type so its state, the number of movies checked, can be maintained between function calls. This wasn't necessary in the original code because of JavaScript's approach to variable scope: the variable checkedMoviesCount
in the outer loop in the parent function is available to the function call below it. (Read more about JavaScript closures.) This is a technique you can use in many places where you need to keep track of values between calls to a function.
movies
is an array of movies through which the function iterates to find the highest scoring.
The function iterates through the specified director's movies. It begins by incrementing the count
counter. Then it iterates through each of the reviews for the movie and adds each review's score to the cumulative score and increments the count of reviews. When it finishes checking the reviews it calculates the average rating for the movie and adds the score to the movie's entry in the list of movies.
When the count of movies checked equals the length of the array of movies the code sorts the array in descending order by average score. The title of the first element in the array, which is the highest-rated movie, is then written to the console output.
Now move up the call stack to the second API call, which retrieves movie reviews.
Insert the following JavaScript code in organizedCallbacks.js below the code you added in the previous step:
This function takes two parameters:
movies
is an array of movies for which you are going to retrieve reviews.
director
is an object containing information about the director of the movies in the movies
array.
The code initializes the toBeChecked
counter, which keeps track of the number of movies remaining to be evaluated.
Then the code iterates through the movies
array. It makes a REST call for each movie
to retrieve a list of reviews, which is stored in the reviews
object and passed to the calculateAverageScore
function.
The last code block you are going to encapsulate is the callback for the first REST request, the code which looks up the movies for a given director:
There are two parameters:
directors
is an array of directors to search.
name
is a string identifying the director for whom you are retrieving reviews. It's used to display the final result. (For more on this, see the Further improvements section below.)
In the body of the function the code saves the id
value for the specified director
and uses it to perform a REST call to retrieve a list of his movies.
The callback function for the findDirector
function is the getReviews
function. When the REST call to the /asynchronous-javascript/directors/{id}/movies endpoint returns a list of movies, the list is used as an argument to the callback function invocation of the getReviews
function in the movies
argument. The name of the director is passed in the name
argument.
The web API call that starts the whole process is the call to the /directors endpoint to get a list of directors. Add the following code below the previous functions:
The web API call gets a list of directors and parses the JSON object into an array. This list of directors is passed to the callback function in the directors
argument. The callback function invocation of findDirector
gets the id
value for Quentin Tarantino.
Testing your reorganized JavaScript callbacks
Verify the new program structure is working by executing the following command-line instruction in the project's root directory:
The output should be:
Critics may differ, but the result reflects the average of the scores in the test API. If you're getting different results, be sure you're using the correct id
value to get the list of movies for the director.
Exploring the advantages of encapsulated callback functions
What are the advantages of encapsulated callbacks? You have decreased number of nested levels in your code from six to three, making it easier to read, maintain, and debug. You can alter or add to the behavior of each function with less impact on the other functions.
You've also made it easier to expand the functionality of your application. Using the new program structure it's easy to find the highest-rated movie for each director in the test set.
Add the following lines to the bottom of the organizedCallbacks.js file:
Execute the program again. You should see a list of the top-rated movies for four directors:
If you haven't been coding along and want to catch to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project directory:
Making further enhancements
The code above is an improvement over nestedCallbacks.js but there's still room for improvement. Here are some ways it can be improved:
- It's upside down. Your programs will be more readable if the code from top to bottom is in the order of execution.
- The functions are highly coupled. The program only works in the structure in which it's currently arranged and the flow of data between functions only permits the program to be structured in one way. This inhibits reusability of the functions and maintainability as requirements change.
- The functions aren't atomic. You can see this easily in a couple places:
- The code knows the name of the director for whom you're going to display the highest-scoring movie before the first API call is invoked, but it gets passed through two function calls.
- The final function call,
calculateAverageScore
, displays the highest-rated movie—but that's "not part of its job description".
Doing more refactoring is a good exercise for you if you're new to JavaScript. The important thing to recognize is how much easier it is to see areas for improvement now that the level of callback nesting has been reduced. When you're doing your own programming and see that you've created a deeply-nested series of callbacks it's a sign you should consider refactoring.
Summary of Organizing JavaScript Callbacks
In this post you learned how to take a deeply-nested set of JavaScript callbacks and refactor them into separate functions, making the code more readable, maintainable, and debuggable. You also saw how reducing the nesting of JavaScript callbacks can make it easier to see other areas in which the code can be improved.
Additional resources
JavaScript: The Good Parts, Douglas Crockford (O'Reilly, 2008) If you are only going to read one book about writing JavaScript, this should be it.
json.org The JSON language specification on one page. Also available in a longer PDF form from ECMA.
GitHub Pages is used to host the test web API used in this post. It's a good thing to know about.
Maciej Treder is a Senior Software Development Engineer at Akamai Technologies. He is also an international conference speaker and the author of @ng-toolkit, an open source toolkit for building Angular progressive web apps (PWAs), serverless apps, and Angular Universal apps. Check out the repo to learn more about the toolkit, contribute, and support the project. You can learn more about the author at https://www.maciejtreder.com. You can also contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.
Updated 2020-01-27
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.