How to Handle Async Code in JavaScript
Geshan Manandhar
Posted on November 23, 2022
The easiest way to understand asynchronous code is to realize that the code does not execute sequentially. This can be difficult to comprehend in JavaScript, especially if you come from a programming language that's synchronous or sequential by default, like PHP.
In this post, you will learn how to write async (also known as 'non-sequential') code in JavaScript efficiently. You'll learn the basics of using callbacks, promises, and the modern async/await style.
Let's get started!
Async Programming: A Quick Intro
Let's start with the basics. There are two execution models in programming languages: synchronous and asynchronous.
The synchronous model is where the next line of code is not executed until the current one is done. Even if the current line of code calls an API that responds in 500ms or reads a 100 MB file, the execution will wait until the line of code fully completes. In other words, in synchronous execution, things happen one at a time, one after the other.
On the other hand, if there are five lines of code, and line two calls an API, it is pushed to the background with a mechanism that lets the main execution know that the API has responded.
While that is happening, lines three to five are also being executed. This is an oversimplified explanation of the asynchronous or async code execution model. In this case, the code does not run line by line in sequence. Things can be put in the background (or queue), and later the result will be known. This model of execution allows multiple things to happen at the same time.
The notion of the execution model is essential here because, depending on the programming language, it can work in a sync way or an async way. For example, in Python, you can write programs in a sync or async fashion — whereas JavaScript is asynchronous by default.
How? You may ask. First, let’s look at a basic example, with a simple synchronous execution showing logs:
console.log("first log line");
console.log("second log line");
console.log("third log line");
The above code is pretty straightforward and will print three log lines one after the other, like below:
first log line
second log line
third log line
Now, let’s make it a bit more interesting. See that JavaScript code does not execute sequentially with a 'waiting 1 second' example:
console.log("first log line");
// 1 second wait
setTimeout(function () {
console.log("third log line - after 1 second");
}, 1000);
console.log("second log line");
Now, when the above code is executed, the output is as follows:
first log line
second log line
third log line - after 1 second
So what happens here? first log line
is printed, then the setTimeout function is called, which executes a function or specified code only after the timer expires. It is set to 1 second — 1000 ms. So this code is pushed to the background to be executed after 1 second.
Then the second log line
console is executed and does its job. After 1 second, the console.log
in setTimeout executes, resulting in the above output. This is how asynchronous code works in JavaScript.
In the next section, we will learn about callbacks with asynchronous JavaScript using a fun Github and Twitter example.
Callbacks with Async JavaScript
“Callback” is a word you have heard in real life: you call a friend, but your friend does not pick up the phone. You reach their voicemail and leave them a message to call you back. Your friend hears the message and, let's say two hours later, they call you back. The concept is the same in programming and JavaScript, especially with asynchronous execution.
We won't go into detail about events and how functions can be passed as parameters in Javascript — that's a topic out of the scope of this article. One thing to mention is that callbacks can be used with synchronous code too.
To understand callbacks in the context of asynchronous code, let's see a code example written for the browser. In this snippet, you will fetch the last Tweet from a person by only using their GitHub username.
First, you will call the GitHub API to get the user's details, including their Twitter username. Then, you will call the Nitter RSS feed to get their last tweet.
We won't use the official Twitter API, as this involves authentication and complicates the process. Given our clear goal, we'll use the below code (on JsFiddle for your reference):
const request = window.superagent;
const parser = new RSSParser();
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/";
request
.get("https://api.github.com/users/abraham")
.set("Accept", "application/json")
.end((err, res) => {
if (!err) {
console.log("Twitter Username: ", res.body.twitter_username);
parser.parseURL(
`${CORS_PROXY}https://nitter.net/${res.body.twitter_username}/rss`,
(err, feed) => {
if (!err) {
console.log(
`The last tweet by ${res.body.twitter_username} is - ${feed.items[0].title}`
);
}
}
);
}
});
console.log("This would log first");
You are using Superagent and RSS parser to get the task done. Superagent and RSS parser both support callback and the Promises API (coming up next). If this code was written for the backend with Node.js, we wouldn't need the CORS proxy. It is needed for the frontend.
The code starts with instantiating superagent as a request, RSSParser, and CORS proxy. Then the first request is made for our user — abraham
— to the GitHub API. On the request end, an anonymous callback is made with err
and res
.
The Twitter username is plucked out of the results only if there is no error. The RSS parser makes another request to parse the RSS URL and get the tweets. This call results in the execution of another callback.
Here's the output of the above code execution:
This would log first
Twitter Username: abraham
The last tweet by abraham is - She-Hulk and Saul Goodman crossover episode?
If you want to dive a bit deeper into how the V8 engine handles async operation, please watch the amazing talk 'Help I'm stuck in an event loop' by Philip Roberts. He explains the event loop in a clear and concise way.
Next, let's learn about callback hell.
Callback Hell in JavaScript
As seen above, writing callback-oriented code for async operations does not feel natural. On top of that, if three or more callbacks are needed to complete a task, our code becomes difficult to write, understand, and eventually manage.
So when you write JavaScript in a way where execution takes place visually from top to bottom with multiple complex levels of callbacks, you land in callback hell territory. One of the easiest ways to understand callback hell is through visuals:
If you don’t want to get into the callback hell zone, you can use promises with async JavaScript.
Async JavaScript with Promises
Promises (also known as futures in other languages) are objects that represent an eventual completion or failure of an asynchronous task, resulting in a value.
Ok, you can understand it better with another analogy.
Let’s say your friend promises to meet you over the weekend. Then Saturday comes. If the friend actually meets you, the promise is "fulfilled". If your friend does not show up for the meeting, the promise is "rejected". Until Saturday, the promise is "pending".
The same concept can explain any asynchronous operation — for example, calling an external URL/API, or reading a file from a disk.
When an API is called, the promise object is pending until an answer comes back. If all goes well, the promise is fulfilled, and the then
method gets the result. In case of failure, the catch
method is called, which has the error object. If you are a visual person, this flow chart by MDN web docs can help you to comprehend this idea.
At this juncture, you will convert the above callback code into promises. Superagent and RSS Parser both already provide a promise-based API. The code to call GitHub's API, then get the user abraham
's Twitter username (one of the popular GitHub users in the US), and call the Nitter RSS for the username will look like this:
const request = window.superagent;
const parser = new RSSParser();
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/";
let twitterUser = "";
request
.get("https://api.github.com/users/abraham")
.set("Accept", "application/json")
.then((res) => {
console.log("Twitter Username: ", res.body.twitter_username);
return res.body.twitter_username;
})
.then((twitterUserName) => {
twitterUser = twitterUserName;
return parser.parseURL(
`${CORS_PROXY}https://nitter.net/${twitterUserName}/rss`
);
})
.then((feed) => {
console.log(`The last tweet by ${twitterUser} is - ${feed.items[0].title}`);
})
.catch((err) => {
console.log(`Error occurred ${err.message}`, err);
});
console.log("This would log first");
It is mainly the same code from an execution point of view, but written differently. It uses promises in place of callbacks now.
If promises are not supported natively by Superagent and RSS Parser, they can be written with JavaScript’s Promise object too, but you don't have to do it on your own. In Node.js, we can also do it with the util.Promisfy function — a topic for another post.
There are some things you should pay attention to in the above code snippet. First, to make the Twitter username available to other then
methods, a twitterUser
variable is set before the request.get
to an empty string. When the value is available, it is set to the last used Twitter username value, as it is still in scope.
In the case of an error in the then
methods, the error is sent to the catch
method, not executing the next then
method.
You see a promise chain here — the first promise calls GitHub, and the second one gets the latest Tweet from Nitter in the second chain. This can be confusing. We can do this more cleanly with async/await syntax — we'll cover this in the next section.
When the above code is executed, it gives output like the below, which is not different than the callback output:
This would log first
Twitter Username: abraham
The last tweet by abraham is - She-Hulk and Saul Goodman crossover episode?
You can also view the promise code snippet on JSFiddle and play around with it.
If promises look interesting to you, please dig deeper into Promise.all and Promise.race for other ways to run promises concurrently.
Now, let's learn about a modern way of working with promises — using async and await syntax.
Async with Await for Promises
Async with await is the modern way of working with promises in a much cleaner style. Async/await is more syntactic sugar on top of promises than a completely new feature of ECMAScript.
Async allows you to write promise-based code as if it was synchronous. An async function will always return a promise, implicitly making it easier to work with. The await keyword can only be used in async functions and waits for the promise to come to a completed state. A complete state here refers to either a 'fulfilled' or 'rejected' state. There are discussions of top-level await as well, but it has not become mainstream as of yet.
With all of that information, now you will convert the above code with promise, then catch to a more "comfortable" async await version, as follows:
const request = window.superagent;
const parser = new RSSParser();
const CORS_PROXY = "https://cors-anywhere.herokuapp.com/";
async function getLatestTweet(githubUsername) {
try {
const gitHubResponse = await request
.get(`https://api.github.com/users/${githubUsername}`)
.set("Accept", "application/json");
console.log("Twitter Username: ", gitHubResponse.body.twitter_username);
const twitterUsername = gitHubResponse.body.twitter_username;
const feed = await parser.parseURL(
`${CORS_PROXY}https://nitter.net/${twitterUsername}/rss`
);
console.log(
`The last tweet by ${twitterUsername} is - ${feed.items[0].title}`
);
} catch (err) {
console.log(`Error occurred ${err.message}`, err);
}
}
getLatestTweet("abraham");
console.log("This would log first");
The above code is similar to the promise code with .then
and .catch
. The main difference here is that all the logic is wrapped in an async function called getLatestTweet
. The promises are unwrapped with an await which, as the keyword says, waits before going to the next line. As the async operations “behave like” sync code, values dependent on the async task can easily be assigned to variables like the twitterUsername
. That is why try/catch also makes more sense here.
In case of any error, as the code acts like sync code, it will be caught in the catch block. For promises using .then
and .catch
, if the .catch
part is missed, errors will get lost. Using async/await makes the code seem synchronous, which is good. Still, overuse of async/await defeats the power of using an async language like JavaScript where "multiple" things can be done simultaneously.
The above code snippet gives out the following output, based on the last tweet by abraham
:
This would log first
Twitter Username: abraham
The last tweet by abraham is - She-Hulk and Saul Goodman crossover episode?
The async/await code snippet is also available on Jsfiddle for your reference.
You have learned three ways of handling asynchronous code in JavaScript. In the following section, you will find out how to make your JavaScript async code more efficient.
Make Your JavaScript Async Code More Efficient
You have seen three variations of doing two HTTP calls, one dependent on the other. There are some cases to consider that can make your code more efficient.
Firstly, use async/await wisely. Do not code JavaScript like PHP, using async/await left, right, and center. This will block the event loop.
For example, if you have to call the GitHub API for five usernames, there is no need to call them one by one.
It can be done concurrently with Promise.all
. With this approach, be careful that you don't hit the API's rate limit, as calls will be made concurrently. Below is a quick example of Promise.all
in action:
const request = window.superagent;
async function getLatestTweets() {
const usernames = ["bdougie", "abraham", "RamiKrispin"];
try {
const requests = usernames.map((username) => {
return request
.get(`https://api.github.com/users/${username}`)
.set("Accept", "application/json");
});
const responses = await Promise.all(requests);
for (const response of responses) {
const responseData = response.body;
console.log(
`Twitter username for GitHub user ${responseData.login} is ${responseData.twitter_username}`
);
}
} catch (err) {
console.log(`Error occurred ${err.message}`, err);
}
}
(async () => {
console.log("Start");
await getLatestTweets();
console.log("Done!");
})();
You can check the code on JSFiddle too. When it runs, it gives the following output:
Start
Twitter username for GitHub user bdougie is bdougieYO
Twitter username for GitHub user abraham is abraham
Twitter username for GitHub user RamiKrispin is Rami_Krispin
Done!
The main mantra here is to think of parts of the code that are not dependent on the previous block. Run parts of the code concurrently, so these parts can be broken into smaller functions. Work is made faster, utilizing all available resources like CPU and memory.
Wrap Up
In this post, we covered the difference between synchronous and asynchronous code and execution models. Then we covered three ways to handle async code in JavaScript, using callbacks, promises, and async/await (with an example calling two URLs).
Finally, we saw an example of how to write async code efficiently in Javascript using concurrency, splitting independent code parts into different functions.
Happy coding!
P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.
P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.
Posted on November 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024