Handling asynchronous code explicitly is mandatory for single-threaded languages (like JavaScript), otherwise, long running operations will result in performance problems described in Synchronous and Blocking.
This tutorial will focus on understanding the three ways we handle asynchronous calls (Callbacks, Promises, and Async/Await) so that you can write scalable JavaScript applications.
This is required reading for the Code Chrysalis Immersive Bootcamp’s precourse — a series of assignments, projects, assessments, and work that all students must successfully complete remotely before embarking on our full-stack software engineering course.
We’ve already seen how higher-order functions can be useful to abstract behaviors while performing synchronous operations, but the most important use of higher-order functions is when doing things asynchronously.
JavaScript can only do one thing at a time (review Synchronous and Blocking) because it is a single-threaded language. This can cause problems when the application is dependent on asynchronous operations.
One of the most common ways developers interact with asynchronous code in JavaScript is through APIs, which stands for Application Programming Interface. You will build your own APIs when you learn servers, and you will also learn to interact with third party APIs.
In general terms, an API is a set of clearly defined methods of communication among various components. APIs receive requests (e.g. "What's the weather?") and sends responses ("22 degrees!").
Sometimes, this communication can take awhile– and that can cause problems when other code depends on the response from those requests. Because of that, JavaScript has built-in ways to handle this situation.
Below, there are three examples of how to handle asynchronous code. Because these examples don't actually use an API, we are using setTimeout to cause a delay. This method takes two arguments– a function, and a number (n). It will then invoke the function after n milliseconds.
Before we show you what to do, here's what NOT to do. Take a look at the function below. It should return a result after three seconds. What happens when you paste the code in your console and run it?
function getCoffee(num) {
setTimeout(() => {
if (typeof num === "number") {
return `Here are your ${num} coffees!`;
} else {
return `${num} is not a number!`;
}
}, 3000);
}
console.log(getCoffee(2));
console.log(getCoffee("butterfly"));
It returns undefined. The console.log() ran before getCoffee() had time to receive a response. That's a problem.
Prior to ES6, asynchronous code was handled through callbacks.
In the example below, getCoffeeCallback is a function that takes two arguments– the number of coffees AND a callback function. This callback function takes two arguments: error and result. Depending on the success of the request, it will return the callback function invoked with either the result or the error (and null for the other argument).
Try running the code below in your console!
function getCoffeeCallback(num, func) {
setTimeout(() => {
if (typeof num === "number") {
return func(`Here are your ${num} coffees!`, null);
} else {
return func(null, `${num} is not a number!`);
}
}, 3000);
}
getCoffeeCallback(2, (error, result) => console.log(error ? error : result));
getCoffeeCallback("butterfly", (error, result) => console.log(error ? error : result));
This time, the code doesn't return undefined. Why? The reason is because of something called the Event Loop. Essentially, the function passed into getCoffeeCallback was added to a queue. JavaScript's event loop works with that queue to execute callback functions at the right time.
Let's look at one more example. Remember how we were reading files synchronously using readFileSync? Because reading files can be a long operation, Node provides both synchronous and asynchronous implementations. The asynchronous version is called readFile.
const fs = require('fs');
const result = fs.readFile('index.js', 'utf8');
console.log(result);
When we use readFile, we get undefined as our result.
If we check the docs, we can see why: readFile expects a third parameter, which is a callback function that gets passed error and result parameters.
Just like our getCoffeeCallback function, let's give it what it wants– a callback!
const fs = require('fs');
fs.readFile('index.js', 'utf8', (error, result) => console.log(error, result));
Yay! It worked.
ES6 normalized a much, much better way to do asynchronous operations– Promises.
Take a look at the refactored code below. You'll notice that the higher order function getCoffeePromise looks almost identical to the getCoffeeCallback function. This time, however, we don't pass in a callback, and we return a Promise (which we create with the new keyword). This Promise gets passed a function with two arguments: a function we call resolve and a function we call reject.
The way getCoffeePromise is called is a little different as well. Since we're no longer passing in a callback, we need to handle the responses another way– by chaining .then() and .catch().
Try running this code in your console!
function getCoffeePromise (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num === "number") {
resolve(`Here are your ${num} coffees!`);
} else {
reject(`${num} is not a number!`);
}
}, 3000);
});
};
getCoffeePromise(2).then(result => console.log(result)).catch(error => console.log(error));
getCoffeePromise("butterfly")
.then(result => console.log(result))
.catch(error => console.log(error));
Note: When you call a promise, you can write it in one line like the first example or on separate lines like the second example. If you choose to put it on separate lines like the second example, just make sure you don't add spaces, comments, or semi-colons in the middle of the chain!
You can read more about Promises here.
ES7 brought another upgrade to the syntax for Promises.
Here is how it works:
What makes this upgrade so great is the await keyword. Normally, writing console.log() outside of a callback or chained Promise method would return undefined. That's not a problem here, because the await keyword STOPS the code from running until that line of code is resolved!
Try it in your browser!
const getCoffeeAsync = async function(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num === "number") {
resolve(`Here are your ${num} coffees!`);
} else {
reject(`${num} is not a number!`);
}
}, 3000);
});
}
const start = async function(num) {
try {
const result = await getCoffeeAsync(num);
console.log(result);
} catch (error) {
console.error(error);
}
}
start(2);
start("butterfly");
Note: You'll notice in this example that we are still returning a Promise in the getCoffeeAsync function. Unfortunately, setTimeout doesn't explicitly support async/await (yet!), so we are adding this wrapper to make our code still work.
You can read more about Async/Await here.
Let's take a look at how all three ways of handling asynchronous JavaScript are used to make the same request to the Pokemon API.
To make these API calls, we are using XMLHttpRequest (which takes a callback) or Fetch (which uses Promises).
Test them out in your browser!
function request(callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "<https://pokeapi.co/api/v2/ability/4/>", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
(function(response) {
callback(JSON.parse(response));
})(xhr.responseText);
} else {
callback(xhr.status);
}
};
xhr.send();
}
request((error, result) => console.log(error ? error : result));
fetch("<https://pokeapi.co/api/v2/ability/4/>")
.then(response => response.json())
.then(jsonResponse => console.log(jsonResponse))
.catch(error => console.log(error));
async function request() {
const response = await fetch("<https://pokeapi.co/api/v2/ability/4/>");
const jsonResponse = await response.json();
console.log(jsonResponse);
}
request();
These look a little different. Why?
These calls return response that aren't immediately useable as JSON– which is common for a lot of APIs.
You will probably need to process JSON when you work with APIs in the future, so make a mental note to remember the .json() method!
Which should you use? Generally, you should stick with using async/await or promises. You should be knowledgeable about the weaknesses of the syntax, but in general, JavaScript syntax gets upgraded for a reason!
async/await (and to a lesser extent, promises) are cleaner looking and more concise, have better error handling, and are easier to debug.
Explore using them in your code!
Code Chrysalis is a Tokyo-based 🗼 coding school providing a full-time and part-time programming courses in English and Japanese. Join us in-person or take our classes remotely. See why we are an industry leader in technical education in Japan 🗾.
We also put on free workshops and events for the community, so follow us for the latest!
👋 Follow us on…
年齢・言語・現在のキャリア・技術的なバックグラウンドは日本での就職活動にどう影響しますか?同じ業界内での中途採用もそうですが、未経験業界への転職はより難しく感じるものです。就職先がどこであれ、転職が成功するためにはいくつかの要素があります。
今回は、コードクリサリスの企業向け研修プログラムを導入していただいたNRI DX生産革新本部 本部長 大元様にお話をお伺いしました。 企業向け研修プログラム導入の経緯や受講後の社員様の変化についてお聞きしました。