Introduction
Asynchronous behavior is common in JavaScript, which might be perplexing for programmers who have only worked with synchronous code. This article will explain what asynchronous code is, as well as some of the challenges that come with utilizing it and how to overcome them.
Synchronous Code
If you have two lines of code (a
followed by b
) in a synchronous program, b
cannot start running until a
has ended.
Imagine yourself in a long queue of people waiting to purchase bus tickets. You can't start purchasing a bus ticket until everyone in front of you have finished buying theirs.Similarly, the individuals in front of you cannot begin purchasing tickets until you have completed yours.
Asynchronous Code
You can have two lines of code in an asynchronous program (a
followed by b
), where a
schedules a job to run in the future but b
executes before that task completes.
Consider yourself as if you were dining at a sit-down restaurant. Other individuals place their meal orders. You may also place a meal order.You don't have to wait for them to complete eating and receive their meal before placing your order. Other individuals don't have to wait for you to finish eating and obtain your food before they can order. As soon as the food is ready, everyone will receive it.
The order in which customers receive their meals is frequently associated with the order in which they placed their orders, although these sequences are not always the same.For example, if you order a steak and I order a glass of water, I'll probably get my order first because serving a drink of water doesn't take nearly as long as preparing and serving a steak.
It's important to note that asynchronous does not imply concurrent or multi-threaded. Asynchronous programming is possible with JavaScript, however it is usually single-threaded.This is similar to a restaurant where a single employee is responsible for all of the waiting and cooking. However, if single employee works swiftly and efficiently switches between jobs, the restaurant seems to have several employees.
Examples
The setTimeout
function is probably the simplest way to asynchronously schedule code to run in the future:
// Say "Hi."
console.log("Hi.");
// Say "Good afternoon" 5 seconds from now.
setTimeout(function() {
console.log("Good afternoon");
}, 5000);
// Say "Hi again!"
console.log("Hi again!");
If you are only familiar with synchronous code, you might expect the code above to behave in the following way:
- Say "Hi".
- Do nothing for five seconds.
- Say "Good afternoon"
- Say "Hi again!"
But setTimeout
does not pause the execution of the code. It only schedules something to happen in the future, and then immediately continues to the next line.
- Say "Hi."
- Say "Hi again!"
- Do nothing for five seconds.
- Say "Good afternoon"
Getting Data from AJAX Requests
Confusion between the behavior of synchronous code and asynchronous code is a common problem for beginners dealing with AJAX requests in JavaScript. Often, they will write jQuery code that looks something like this:
function getData() {
var result;
$.get("process.php", function(response) {
result = response;
});
return result;
}
var result = getData();
console.log("The result is: " + result);
This does not behave as you would expect from a synchronous point-of-view. Similar to setTimeout
in the example above, $.get
does not pause the execution of the code, it just schedules some code to run once the server responds. That means the return result
; line will run before result = response
, so the code above will always print "The data is: undefined".
Asynchronous code needs to be structured in a different way than synchronous code, and the most basic way to do that is with callback function
Callback Functions
Let's say you call your friend and ask him for some information, say, a mutual friend's mailing address that you have lost. Your friend doesn't have this information memorized, so he has to find his address book and look up the address. This might take him a few minutes. There are different strategies for how you can proceed:
- (Synchronous) You stay on the phone with him and wait while he is looking.
- (Asynchronous) You tell your friend to call you back once he has the information. Meanwhile, you can focus all of your attention on the other tasks you need to get done, like folding laundry and washing the dishes.
In JavaScript, we can create a
callback function
that we pass into an asynchronous function, which will be called once the task is completed.
That is, instead of
var result = getData();
console.log("The result is: " + result);
we will pass in a callback function to getData
:
getData(function(result) {
console.log("The result is: " + result);
});
Of course, how does getData
know that we're passing in a function? How does it get called, and how is the result
parameter populated? Right now, none of this is happening; we need to change the getData
function as well, so it will know that a callback function is its parameter.
function getData(callback) {
$.get("process.php", function(response) {
callback(response);
});
}
You'll notice that we were already passing in a callback function to $.get
before, perhaps without realizing what it was. We also passed in a callback to the setTimeout(callback, delay)
function in the first example.
Since $.get
already accepts a callback, we don't need to manually create another one in getData
, we can just directly pass in the callback that we were given:
function getData(callback) {
$.get("process.php", callback);
}
Callback functions are used very frequently in JavaScript and, if you've spent any amount of time writing code in JavaScript, it's highly likely that you have used them (perhaps inadvertently). Almost all web applications will make use of callbacks, either through events (e.g. window.onclick
), setTimeout
, and setInterval
, or AJAX requests
.
Common Problem: Trying to Avoid Asynchronous Code Altogether
Some people decide that dealing with asynchronous code is too complicated to work with, so they try to make everything synchronous. For example, instead of using setTimeout
, you could create a synchronous function to do nothing for a certain amount of time:
function pause(duration) {
let start = new Date().getTime();
while (new Date().getTime() - start < duration);
}
Similarly, when doing an AJAX call, it is possible to set an option to make the call synchronous rather than asynchronous (although this option is slowly losing browser support). There are also synchronous alternatives to many asynchronous functions in Node.js.
Trying to avoid asynchronous code and replacing it with synchronous code is almost always a bad idea in JavaScript
because JavaScript only has a single thread (except when using Web Workers). That means the webpage will be unresponsive while the script is running. If you use the synchronous pause
function above or a synchronous AJAX call, then the user will not be able to do anything while they are running.
The issue is even worse when using server-side JavaScript: the server will not be able to respond to any requests while waiting for synchronous functions to complete, which means that every user making a request to the server will have to wait to get a response.
Common Problem: Scope Issues with Callbacks Inside Loops
When you create a callback inside of a for-loop, you might encounter some unexpected behavior. Think about what you would expect the code below to do, and then try running it in your browser's JavaScript console.
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
The code above is likely intended to output the following messages, with a second of delay between each message:
1 second(s) elapsed.
2 second(s) elapsed.
3 second(s) elapsed.
But the code actually outputs the following:
4 second(s) elapsed.
4 second(s) elapsed.
4 second(s) elapsed.
The problem is that console.log(i + " second(s) elapsed");
is in the callback of an asynchronous function. By the time it runs, the for-loop will have already terminated and the variable i
will be equal to 4
.
There are various workarounds to this problem, but the most common one is to wrap the call to setTimeout
in a closure, which will create a new scope with a different i
in each iteration:
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
})(i);
}
If you are using ECMAScript6 or later, then a more elegant solution is to use let
instead of var
, since let
creates a new scope for i
in each iteration:
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
Common Problem: Callback Hell
Sometimes you have a series of tasks where each step depends on the results of the previous step. This is a very straightforward thing to deal with in synchronous code:
var text = readFile(fileName),
tokens = tokenize(text),
parseTree = parse(tokens),
optimizedTree = optimize(parseTree),
output = evaluate(optimizedTree);
console.log(output);
When you try to do this in asynchronous code, it's easy to run into callback hell, a common problem where you have callback functions deeply nested inside of each other. Node.js code and front-end applications with lots of AJAX calls are particularly susceptible to end-up looking something like this:
readFile(fileName, function(text) {
tokenize(text, function(tokens) {
parse(tokens, function(parseTree) {
optimize(parseTree, function(optimizedTree) {
evaluate(optimizedTree, function(output) {
console.log(output);
});
});
});
});
});
This kind of code is difficult to read and can be a real pain to try to reorganize whenever you need to make changes to it. If you have deeply nested callbacks like this, it is usually a good idea to arrange the code differently. There are several different strategies for refactoring deeply nested callbacks.
Split the Code into Different Functions with Appropriate Names
You can give names to the callback functions so that you can reference them by their names. This helps to make the code more shallow, and it also naturally divides the code into small logical sections.
function readFinish(text) {
tokenize(text, tokenizeFinish);
}
function tokenizeFinish(tokens) {
parse(tokens, parseFinish);
}
function parseFinish(parseTree) {
optimize(parseTree, optimizeFinish);
}
function optimizeFinish(optimizedTree) {
evalutate(optimizedTree, evaluateFinish);
}
function evaluateFinish(output) {
console.log(output);
}
readFile(fileName, readFinish);
Create a Function to Run a Pipeline of Tasks.
This solution is not as flexible as the one above, but if you have a simple pipeline of asynchronous functions you can create a utility function that takes an array of tasks and executes them one after another.
function performTasks(input, tasks) {
if (tasks.length === 1) return tasks[0](input);
tasks[0](input, function(output) {
performTasks(output, tasks.slice(1)); //Performs the tasks in the 'tasks[]' array
});
}
performTasks(fileName, [
readFile,
token,
parse,
optimize,
evaluate,
function(output) {
console.log(output);
}
]);
Tools for Dealing with Asynchronous Code
Async Libraries
If you are using lots of asynchronous functions, it can be worthwhile to use an asynchronous function library, instead of having to create your own utility functions. Async.js is a popular library that has many useful tools for dealing with asynchronous code.
Promises
Promises are a popular way of getting rid of callback hell. Originally it was a type of construct introduced by JavaScript libraries like Q and when.js, but these types of libraries became popular enough that promises are now provided natively in ECMAScript 6.
The idea is that instead of using functions that accept input and a callback, we make a function that returns a promise object, that is, an object representing a value that is intended to exist in the future.
For example, suppose we start with a getData
function that makes an AJAX request and uses a callback in the usual way:
function getData(options, callback) {
$.get("process.php", options, function(response) {
callback(null, JSON.parse(response));
}, function() {
callback(new Error("AJAX request failed!"));
});
}
// usage
getData({name: "John"}, function(err, data) {
if(err) {
console.log("Error! " + err.toString())
} else {
console.log(data);
}
})
We can change the getData
function so that it returns a promise. We can create a promise with new Promise(callback)
, where callback
is a function with two arguments: resolve
and reject
. We will call resolve
if we successfully obtain the data. If something goes wrong, we will call reject
.
Once we have a function that returns a promise, we can use the .then
method on it to specify what should happen once resolve
or reject
is called.
function getData(options) {
return new Promise(function(resolve, reject) { //create a new promise
$.get("process.php", options, function(response) {
resolve(JSON.parse(response)); //in case everything goes as planned
}, function() {
reject(new Error("AJAX request failed!")); //in case something goes wrong
});
});
}
// usage
getData({name: "John"}).then(function(data) {
console.log(data)
}, function(err) {
console.log("Error! " + err);
});
The error handling feels a bit nicer, but it's difficult to see how we are making things any better given the size of the function. The advantage is clearer when we rewrite our callback hell example using promises:
readFile("fileName")
.then(function(text) {
return tokenize(text);
})
.then(function(tokens) {
return parse(tokens);
})
.then(function(parseTree) {
return optimize(parseTree);
})
.then(function(optimizedTree) {
return evaluate(optimizedTree);
})
.then(function(output) {
console.log(output);
});
Conclusion
At this point, you should be familiar with strategies for confronting some of the difficulties that arise when using asynchronous code.
If you've reached this point, thank you very much. I hope that this tutorial has been helpful for you and I'll see you all in the next.
If you like my work, please consider
so that I can bring more projects, more articles for you
If you want to learn more about Web Development, feel free to follow me on Youtube!
Top comments (0)