The Path to Conquering Async JavaScript
Ryan
Posted on October 18, 2017
Yoooo, I'm glad you're here! We'll be discussing how to conquer asynchronous JavaScript! As I've been trying to teach myself Node.js and build some things, I've found that async calls aren't the most trivial to learn to deal with. Async JavaScript actually takes a good amount of thought to fully comprehend. I hope to pave a path that makes understanding how to deal with async calls faster and easier.
You can find all of the code used in this article on GitHub.
What's the Problem?
JavaScript is a synchronous language, which means it is single-threaded, so it only runs one block of code at a time. A problem occurs when we want to make some sort of async call, which is a multi-threaded. The problem is that when our JavaScript calls an async function - our JavaScript continues to run, although there is a block of code running somewhere else. I run into this problem the most when I am dealing with API requests.
Here's an example -
- The JavaScript block starts
- An API request is made
- JavaScript code continues AND the API request processes
- JavaScript uses the request's response before the response is returned
Notice how there are two number threes? That's the problem. The JavaScript code continues to run as the request is being made. This means it's possible for the JavaScript to attempt to use the request's response value before it's available, and we take the L.
The Goal
The goal is to be able to call async functions in a synchronous fashion - the calls should wait for the one before it to finish before executing:
Which will look something like this when there are multiple async calls:
var a = await asyncToGetA();
var b = await asyncToGetB(a);
alert(b);
Using Callbacks
What's a Callback?
So how do we overcome this issue? Well, let's first take a look at callback functions so that we can get a glance at a potential fix. Callbacks are a way of telling the code to run a function after another function is finished, if your code isn't making too many async calls then this is an ok option to use. This is achieved by passing a function into another function as an argument, and then calling the argument function at the end of the one it's passed to.
Let's say we have a function, runThisFirst()
, that we want to run before another function, runThisSecond()
. runThisFirst()
will simulate an async call with setTimeout()
and set x
to 5. Once that's finished, runThisSecond()
will run. Since we want runThisSecond()
to run after runThisFirst()
, we are going to pass it as the callback function:
// Define functions
var runThisFirst = function(callback){
setTimeout(function(){
x = 5;
callback(); // runThisSecond is called
}, 3000);
}
var runThisSecond = function(){
alert(x);
}
// Run functions, pass runThisSecond as the callback argument
var x;
runThisFirst(runThisSecond);
You can run this code snippet on JSFiddle.
Callback Chaining
If callbacks fix our async issue, then can't we just chain callbacks together? You can, but it gets scary. There's this concept of Callback Hell where the callback JavaScript code turns into a pyramid shape, making it messy and hard to understand.
Here's a minimalist example of what the skeleton of a Callback Hell pyramid looks like:
function one() {
setTimeout(function() {
console.log('1. First thing setting up second thing');
setTimeout(function() {
console.log('2. Second thing setting up third thing');
setTimeout(function() {
console.log('3. Third thing setting up fourth thing');
setTimeout(function() {
console.log('4. Fourth thing');
}, 2000);
}, 2000);
}, 2000);
}, 2000);
};
One of the best programming practices is writing readable code, and callbacks can stray us away from that when chaining too much. To avoid this, we're going to look into Promises and Async/Await.
Promises
A promise
function is a function that promises to return a value. This allows you to associate code with async calls, all by having the async calls be apart of the Promise. This is where we can make our API calls. :) Here's how they work:
var somePromise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function(){
resolve(x); // Return your promise!
}, 3000);
});
You can see the Promise
constructor has two parameters: resolve
, and reject
. If everything within the Promise goes according to plan (there are no errors), resolve
is called, which returns some value for the Promise. If an error occurs, the Promise should call reject
and return the error. For this example, reject
is not being called.
Now, let's try to run something that depends on this Promise to see if it waits for the x
value to be resolved before executing. We can do this by using the .then
function:
var somePromise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function(){
resolve(x); // Return your promise!
}, 3000);
});
somePromise.then((somePromisesReturnValue) => {
alert("Check it out: " + somePromisesReturnValue);
});
You can run this code snippet on JSFiddle.
Check it out! Things are already looking cleaner and easier to understand. Nice job. :) But now, what if one Promise depends on another Promise? We'll have to chain Promises together.
In order to pass values from one Promise to another, we are going to wrap the Promise within a function like so:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
Promise Chaining
Now we can write another Promise, anotherPromise()
, which is going to take the return value of somePromise()
and add 1 to it. This function is going to have a shorter setTimeout()
, so we can tell that it waits for somePromise()
to resolve before running. Notice how we pass somePromisesReturnValue
as an argument:
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
Now, all we have to do is use the .then
function in order to call these Promises synchronously:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
somePromise().then(anotherPromise);
You can run this code snippet on JSFiddle.
Heck yeah! You can see that anotherPromise()
waited for somePromise()
's return value, 5, before it executed its code. Things are really looking up. :)
Async/Await
Awesome! So we're done, right? Nope, but we're close! If we take our code from the last section, and try to assign the return value from the Promise chain, we can see that the rest of the code isn't waiting for the entire Promise chain to resolve. "[object Promise]" is alerted first.
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
alert("Resolving: " + y);
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
var chainValue = somePromise().then(anotherPromise);
alert(chainValue); // This is executing before chainValue is resolved
You can run this code snippet on JSFiddle.
How do we make the rest of the code wait?! That's where async
and await
come in. The async
function declaration defines an async function, a function that can make async calls. The await
operator is used to wait for a Promise to resolve, it can only be used inside an async
function.
Mission Accomplished
Instead of using .then
, let's create a main()
function so that we can make calls like the goal we had at the beginning of the article:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(y); // Return your promise!
}, 1000);
});
return promise;
}
const main = async () => {
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
main();
You can run this code snippet on JSFiddle.
Look how pretty that main function is :') beautiful. And there you have it, a nice looking main function that isn't a pyramid. Congratulations!
Adding Broad Error Handling
You may want to add some error handling within your Promises themselves while using the reject
callback, but you can also add overall error handling with a try/catch inside of the main()
function that will catch any errors thrown throughout all of the code used within the main()
function:
const main = async () => {
try{
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
catch(err){
alert('Oh no! Something went wrong! ERROR: ' + err);
}
}
We can check this by throwing an error within our anotherPromise()
:
function somePromise() {
var promise = new Promise((resolve, reject) => {
var x = 5;
// Now wait a bit for an "async" call
setTimeout(function() {
resolve(x); // Return your promise!
}, 3000);
});
return promise;
}
function anotherPromise(somePromisesReturnValue) {
var promise = new Promise((resolve, reject) => {
var y = somePromisesReturnValue + 1; // 6
throw 3292; // ERROR CODE BEING THROWN HERE
setTimeout(function() {
resolve(y);
}, 1000);
});
return promise;
}
const main = async () => {
try{
var a = await somePromise();
var b = await anotherPromise(a);
alert(b);
}
catch(err){
alert('Oh no! Something went wrong! ERROR: ' + err);
}
}
main();
You can run this code snippet on JSFiddle.
Review
I'm glad that we were able to make it this far and come up with a pretty basic path for overcoming JavaScript async problems! We took a look at fixing async issues with callbacks, which can work if there isn't too much complexity. Then we dove into solving the problem with combining Promises and Async/Await! Finally, we talked about how to broadly handle errors. If you would like to learn more about error handling with Promises and Async/Await, I suggest you check out some documentation: Promise.prototype.catch() and await.
If you'd like to work on something where this async functionality could be useful, think about checking out my article on how to make a Twitter bot with Node.js. :)
Posted on October 18, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.