This post was originally published on my website and is also available in RU and ZH. Check it out!
JavaScript is an amazing language that can be used anywhere - it runs natively in the browser, can power up mighty server, mobile, and desktop applications. Regular updates approved by the ECMA make its syntax and built-in functionality even more pleasant to work with. Being an extremely beginner-friendly programming language (with the power of just hitting Ctrl + Shift + J
in a browser window to start a sick coding sesh), JavaScript, however, brings some twists to the table that may be mind-boggling and repelling. Some of these "tricky" hard-to-grasp parts can be just memorized and copypasted, but at some point (e.g. when learning a new library or implementing a certain feature) they can backfire and backfire hard. Asynchrony is certainly one of those concepts.
If you’ve tried hard for some time to tame the asynchronous beast, run across dozens of "setTimeout" examples but feel like you haven't moved an inch forward: worry not, you will certainly nail it. I hope, this post might be one of those "clicks" that make previously impossible crystal clear.
Some useful links just in case:
- MDN tutorials on asynchronous JavaScript
- W3Schools introduction to AJAX
- What the heck is event loop
- Callback hell
This post is aimed primarily at the beginners, who have struggled with asynchronous JS for a while, but maybe some of the more advanced readers might find it useful.
Prerequisites: understanding the basic syntax of JavaScript, Node.js installed. In this tutorial, we are going to use some modern JavaScript syntax, but if you’ve been learning JavaScript so far with a little bit dated material (which can still be perfectly relevant), don't worry - there won't be much. Just in case - a quick introduction to some modern JS features by Flavio Copes and let and const keywords tutorial by Victoria Crawford.
A step back
Before we begin, there’s a certain point I’d like to clarify: even though there are a lot of amazing JavaScript tutorials and articles, some of them can be harsh on a beginner. Since JavaScript is the first choice for many people starting their journey into web- and software development, many tutorials are eager to jump into the action and teach how to manipulate the web-page. Not being bad on its own, this can evoke some problems in the long run: copying and pasting code snippets can take us far, but only so far.
When it comes to the majority of tutorials covering asynchronous JavaScript, there are usually two points at which they’re lacking, making the whole topic overly-vague: fully clarifying the whole purpose for asynchronous code in the first place (apart from really dry definitions), and providing easy-to-read examples that can be understood by a beginner (and this is vital since reading code that jumps up and down can be quite an experience).
Asynchrony is by no means easy, it can be frustrating, especially for someone relatively new to web development. You feel like everything else is perfectly tackled down: you've got your HTML and CSS knowledge in check, JS variables and data types are no big deal, adding and removing elements from the DOM seems easy, but all of the sudden, you get stuck. It may be a good idea to take a little step back in order to make a big step forward.
First, we’re going to talk about data in applications to understand the need for, and purpose of the asynchronous code, and then we’re going to jump into some code to see how it can be implemented in JavaScript using callbacks.
Data in a program
Computer programs do not exist in a vacuum. Well, most of the more useful computer programs don’t. Without getting too formal, we can assume that any application or program is, basically, an abstraction over receiving some data as an input and bringing it to the output.
Data can come in all forms and shapes, and from the data-source point of view, we can roughly divide all the data our application needs into two categories: “internal” data that is “hardcoded” and immediately available the moment out program starts, and “external” data that has to be loaded in the application’s memory during the execution process.
The fundamental difference between these two “types” of data is speed. The already-in-memory data is extremely fast, however, getting the external data is much, much slower. But at the same time, external data is much more interesting for us.
A hardcoded array of names will be loaded blazing fast, but it won’t preserve any changes: as soon as our program terminates, all of our changes will be lost. However, an array of names received from a local database, a file system, or some external data source via the Internet is much more exciting and useful to work with. But in comparison, this process is much slower.
"Slow" always sounds bad when it comes to software development. No one wants to use a slow mobile app or to browse a slow website. Generally, there are two approaches that are used in programming (sometimes combined) to solve this “slow data problem” - multithreading and asynchrony.
Multithreading has been one of the most widespread approaches to dealing with “slow” data and operations, used in languages like Java. In multithreading, we launch a separate process (a “thread”) to execute a “slow” operation in the “background” without making our application freeze. For example, in an Android application, our “main thread” would usually track down touches to the screen, and if some operation after touching a button is “slow” (i.e. it involves accessing some external data source or a heavy computation) this operation will be executed on a separate thread. Updating a feed in a social network app, or calculating the velocity of an enemy after an impact in a mobile game - all these operations would usually run on a separate thread in an Android application.
Launching separate threads is not a stranger to JavaScript applications either: service workers for example, can help us take our web-applications to another level. However, this technique is fairly advanced and can be an overkill for most of the “slow” operations a web-application would usually face. In the JavaScript world, using asynchronous programming is much more common.
Asynchrony is aimed at roughly the same task: execute some time-consuming operation without blocking the user interface. When we upload an image on a website or hit a submit button to post a comment, an asynchronous operation happens, and if done correctly, our webpage stays active and responsive during the operation - we can scroll up and down, in some cases visit other pages of the application and interact with other buttons. However, even though multithreading and asynchrony might be used for the same kind of operations, they are fundamentally different at the implementation level.
In asynchronous programming, we have a single thread that runs constantly during the whole execution time of the program, “waiting” for the events, user input for example. To put it roughly, this process constitutes a “loop” of “events”, an event-loop. On each cycle or iteration of the loop, it “catches” and starts executing commands, i.e. our code directives. What makes it special, is that if a certain line of code takes a long time to execute and return some value, the operation depending on it can be “postponed” to the next iteration of the loop.
For instance, we want to load a list of users from the server via HTTP request and display them on our screen. At large, this operation consists of two steps, one being reasonably slow and one being blazing fast:
1) Make a request to the server, get some value in the response (usually in JSON, a special data format), convert the received value into something our application can work with (usually, an array of JavaScript Objects);
2) Iterate through the array of Objects, create an HTML element on each iteration and append it to the webpage.
Between these two operations, there would be at least one iteration of the event-loop. On the first one, the “get-data-from-the-server” function would be invoked, on the second one - “display-data-to-the-screen” function would be called with the received data.
The same principle can be applied to the Node.js JavaScript applications that live outside the browser. Node.js is a runtime that makes it possible to run JavaScript programs on a machine outside the browser, and one of the major tools for the development of powerful JS applications. A Node.js application typically has access to the part of the local file system it is put into (usually, the application folder), and it can read and write different types of files, thus it’s capable of sending different types of files to the client, and getting them from the client, as well: when we upload an image to such a server, it has to write it to the file system via asynchronous operation.
When we open a JavaScript web-application in the browser, an event-loop starts. When we launch our Node.js server-side application, an event-loop starts. And as a rule of thumb, any calculation-heavy or utilizing external data source operation should be made asynchronous. On-page HTTP requests should be asynchronous. Connecting to the database should be made asynchronous. Writing to and reading from the file system should be made asynchronous.
The implementation of asynchronous operations in JavaScript, that’s where we need to refresh our understanding of the language syntax and structure, especially seemingly easy concepts like function declaration and invocation.
Function declaration and invocation
“In JavaScript, functions are the first-class citizens”. Wait, wait, wait. If you were ready to hit the display with something heavy or, even worse, end all this asynchrono-whatever mumbo-jumbo right now, wait a little bit, I do feel you on this one. This phrase has been totally abused by dozens of tutorials you’ve probably read so far, and yes, it hardly clarifies anything by itself.
What it means in practice, is that in JavaScript we can pass functions as arguments to other functions. And this can be really hard to spot at first, even if you’ve gazed at some code snippets for hours.
The actual problem for understanding is, the majority of functions that deal with time-consuming operations (e.g. window.fetch()
or fs.readFile()
) are already built into the browser API and Node.js standard library, so it is really hard to understand how they work. We will write an asynchronous function of our own and pass another function as an argument to it. We will manually postpone the invocation of the latter function to the next iteration (or tick) of the event loop using .nextTick()
method of the process object (that literally stands for the process our program is running on).
With Node.js installed on your system, open your text editor or IDE of choice (I prefer VSCode), create a new file called “pseudoAsyncFunction.js”, and let’s start some coding!
// Declare a function
function slowFunction(a, b, fastFunction) {
console.log("Time-consuming operation started");
let c = a + b;
process.nextTick(function() {
console.log("...calling fastFunction in the next iteration of the event loop");
fastFunction(c);
});
}
We declared a function called slowFunction
that takes three parameters: a
, b
and a mysterious fastFunction
, that is going to be called inside the slowFunction
.
We start a "time-consuming" operation (a totally fake one, here we simply get the sum of a
and b
) and store its result in a variable c
that, in its own turn, is being passed to the fastFunction
as an argument.
In the next line, we call process.nextTick()
method, in which we pass and define an anonymous function, in whose body we finally call our fastFunction
with c
passed as a parameter.
Already at this point things might start to get a little messy (what’s up with this .nextTick
?!), but worry not. Unfortunately, asynchronous programming is hard to illustrate with an example without asynchronous programming. A vicious cycle.
Let’s try and call our brand new slowFunction
and see what it’s capable of! Under the previous lines of code add the following:
console.log("Program started");
// Call our slowFunction with parameters: 1, 2,
// and define actual "fast function" to be called with the result c
// as its parameter
slowFunction(1, 2, function actualFastFunction(c) {
console.log("The result of the time-consuming operation is:");
console.log(c);
console.log("Program terminated");
});
console.log("This function is being called after the slowFunction");
Open the terminal (Ctrl + Shift + ~
in VSCode) and from the folder containing our working file run the following command:
node pseudoAsyncFunction.js
The output of our program would be:
Program started
...Time-consuming operation started
This function is being called after the slowFunction
...calling fastFunction in the next iteration of the event loop
The result of the time-consuming operation is:
3
Program terminated
The important piece is, our actualFastFunction
was called after the line of code:
console.log("This function is being called after the slowFunction");
Synchronous code is being executed from top to down, and we would expect the line of code above to run last, but asynchronous code behaves differently. The line:
This function is being called after the slowFunction
Is being printed to the console output on the FIRST iteration, or tick, of the event-loop, while the lines:
...calling fastFunction in the next iteration of the event loop
The result of the time consuming operation is:
3
Program terminated
are being printed on the second iteration, since they were postponed with process.nextTick()
.
Take a look at our code once again. Let’s analyze what we did here:
- We declared the
slowFunction
that takes 3 arguments, one of which we calledfastFunction
- We directed
fastFunction
to be called in the very end of theslowFunction
, postponed its execution by placing it insideprocess.nextTick()
and passed variablec
that contains the value of the “time-consuming operation” as its parameter; - We called our slowFunction with 1 and 2 as the first two arguments, and defined a new function called
actualFastFunction
inside the parenthesis. And this function is the one that would be called after the “time-consuming” operation has finished.
What’s important to note here, is that in the invocation of our slowFunction
, we did not call actualFastFunction, we defined it knowing the shape this function should take. We know it takes a single parameter, so we designed it to take one. This could be any other function that would take one argument (c
) and do something with it as soon as the operation to get the c
completes.
We could call our slowFunction like this, naming its parameter differently:
slowFunction(1, 2, function anotherActualFastFunction(resultOfSlowFunction) {
console.log("The result of the time consuming operation is: " + resultOfSlowFunction);
console.log("Program terminated");
});
or use an anonymous function:
slowFunction(1, 2, function (c) {
console.log("An anonymous function reporting!");
console.log("The result of the time-consuming operation is: " + c);
console.log("Program terminated");
});
or use a fancy arrow-function and some newer JS syntax:
slowFunction(1, 2, (c) => {
console.log(`Here’s the value of c - ${c}. Sincerely yours, fancy arrow function`);
console.log("Program terminated");
});
or we can pre-define our fastFunction and then pass it to the slowFunction:
function separatelyDefinedFastFunction(c) {
console.log("Hey, I am defined separately!");
console.log("The result of the time consuming operation is: " + c);
console.log("Program terminated");
}
slowFunction(1, 2, separatelyDefinedFastFunction);
Please note that we don’t put parentheses after our separatelyDefinedFastFunction
in the braces when invoking the slowFunction
- we are not calling it yet, it is going to be called inside the slowFunction
. Otherwise, this would give us an unexpected result: in the strict mode, separatelyDefinedFastFunction
would be called with non-existing yet variable c
as its parameter and throw an error, in the non-strict mode, it would be called with c
being undefined
, and it would return no value, making the slowFunction
throw an error: it expected to have a function to call, but now it received nothing.
Now, try to tweak our code a little bit on your own! Maybe fastFunction
can do some calculations with the received value? Or, at some point, will it take some function as a parameter itself? Try to make some changes, get a couple of successes and errors (which is certainly not the thing to be afraid of), and move on to the next section, we’re going to talk about callbacks.
Call me maybe!
The technique we’ve just seen above is the so-called callbacks that you've probably already encountered before. Callback functions literally stand for their name: they are "called back" by the outer function ("the slow function") when the time-consuming operation has finished.
In this case, our fastFunction
and its variations are all callback functions - functions that are passed as parameters to other functions and called somewhere inside them. This is what the gear-grinding phrase about “first-class citizens” basically means.
Callback functions are one of the first techniques used in JavaScript for asynchronous operations; however, they are not used for just this. Many built-in methods in JavaScript, for instance, JS Array higher-order functions, rely heavily on callbacks: when we invoke myArray.map() or myArray.forEach() these methods require a function as a parameter - a callback function to be called on each iteration of the higher-order function. If you’re not familiar with higher-order functions yet or you’ve been using them without much understanding of how they actually work, I strongly recommend taking a look at them after finishing this tutorial (for instance, check out this video by amazing Brad Traversy).
What’s important to understand, is that callbacks are not part of some external library or a special jitsu: they are just one of the natural ways of writing code in JavaScript, along with closures and other techniques wrongly accused of being “mysterious”.
Actually, you've probably already seen some articles claiming that using callbacks for asynchronous operations is obsolete, and now we all should use Promises and async/await for asynchronous operations. That’s partly true - in relatively complicated operations, these two are much more readable and pleasant to work with, but here’s the catch:
Both of them are based on callbacks (even though syntax looks completely different).
Promises can be called “callbacks on steroids” and async/await is a sort of “syntactic sugar” above Promises. Without understanding callbacks, their benefits and drawbacks, it’s easy to find oneself in a situation when you get a nice power drill and use it as a manual screwdriver, never pushing the button. Definitely not so productive.
Callbacks are an integral part of organizing code in JavaScript. From a certain point of view, many JavaScript applications are a huge flow of functions inside other functions. This is a rough interpretation, but some frameworks like Express (a de-facto standard tool for building server-side applications in Node.js) are literally based on functions sitting inside other functions. Understanding this so-called “middleware” (which are literally functions-in-the-middle) architecture depends on getting the best of callbacks.
In the section above, we mentioned having a possible error in our function: what if some part of the input is wrong? An unhandled error would break our program. In order to avoid passing wrong values to the functions, some useful conventions for writing asynchronous functions and functions with callbacks have evolved, first starting with the Node.js applications and later on applied to JavaScript programming in general. They are:
- A callback usually comes last, after all other parameters in a function;
- The first argument of a callback is
err
, standing for a possible error, and the second argument is the expected value;
Let’s rewrite our slowFunction
to fit these conventions, add some error checks, and rename our fastFunction
to callback
:
function slowFunction(a, b, callback) {
// declaring our variables
let error = null;
let c = null;
console.log('...time consuming operation started');
// check if there's a callback
if (!callback || !(callback instanceof Function)) {
throw new Error('A problem with callback!');
}
// check a and b for an error
if (!a || !b || !Number.isInteger(a) || !Number.isInteger(b)) {
error = new Error('Wrong input!');
} else {
c = a + b;
}
process.nextTick(function() {
console.log('...calling fastFunction in the next iteration of the event loop');
callback(error, c);
});
}
Here we’ve tweaked our function a little bit: now we have two variables we are going to invoke our callback function with: error
and c
, both of them initially null
. We’ve added two simple checks for an error using logical ||
(or) operator. First, we check, if the callback exists and whether it is a function. If it is not, we throw an error, terminating the function execution. Then, we check a
and b
for an error: if there’s no a, or there’s no b, or a is not an Integer, or b is not an Integer, we create a new JS Error Object, pass a String 'Wrong input' as its .message
attribute, and assign it to the variable error
, while our variable c
stays null. Otherwise, if the input is correct, the error
variable remains null
, while c
is assigned to the value of a + b
. We call our callback function and pass error
and c
as its parameters at the next iteration of the event-loop.
Now, if we can call our slowFunction like this:
slowFunction(1, 2, function actualCallback(err, c) {
if (err) {
console.log(err.message);
} else {
console.log(`The result is: ${c}`);
}
});
Here we pass parameters 1 and 2, and define the callback function to call: our actualCallback
function (which, as we remember, could have been defined anywhere and passed here as a parameter without parenthesis). Our actualCallback
function tekes two arguments: a possible error and the return value of the “time-consuming” operation. In the function body, we first check for an error, and if the error is not null
(i.e. the error is present) we output the value of its .message
property to the console. Otherwise, if the error is null
, it means that c
holds something meaningful and we output it to the console (once again, note the fancy string interpolation: this is a very nice technique to have in your arsenal).
Let’s try and call our slowFunction
with some erroneous parameters:
slowFunction(1, "Some silly string", function actualCallback(err, c) {
if (err) {
console.log(err.message);
} else {
console.log(`The result is: ${c}`);
}
});
This time our output will be:
Wrong input!
Since the err
parameter is now an Error object with the .message
of "Wrong input" and the c
is null
.
This convention is really handy and used in many built-in and external JavaScript libraries. However, it has a considerable drawback: as our operations grow and become more complex, with callbacks passed inside callbacks (which is much more common than it may seem - asynchronous operations rarely come alone) so does the number of error checks, leading to the so-called callback hell problem. The above-mentioned Promises and async/await are one of the tools that are here to make our code more readable and maintainable, but for now, we need to see the full potential of callbacks in action.
Most of the time, we don’t need to write our own asynchronous functions and manually postpone the invocation of our callbacks with process.nextTick()
. The majority of the functions we would need are pre-defined for us: fs.writeFile()
, fs.readFile()
, window.fetch()
, and many others. Documentation (and handy IDE snippets) will help us understand what arguments, including the passed-in functions, are expected from us.
Now we’re going to take a look at some server-side and client-side “real-world” examples: one involving the filesystem (fs
) module of Node.js and another using the methods of the XMLHttpRequest
Object available in the browser.
Server-side example
For a relatively long time, JavaScript has been the language of the browsers, however, the idea of writing the same language both client- and server-side has been in the air for a while, when in 2009 Node.js, a runtime for JavaScript, was launched. Since then, JavaScript has gone through tremendous changes, becoming an extremely versatile and powerful language with lots of wonderful libraries and frameworks for the development of client, server, desktop, and mobile applications. It is safe to say that Node.js and NPM played a huge part.
Even though in 2020 we have new competitors to Node.js (for instance, Deno - a system developed by one of the Node.js original creators), it remains one of the major tools for JavaScript applications development with immense capabilities.
One of the most common use cases for Node.js are server-side applications. Roughly speaking, a server-side application should be able to:
1) receive and handle an HTTP request;
2) get some data from the local machine according to the request;
3) send the data in HTTP response.
The source of data on the local machine might be a database or simply the part of the file system available for the application. Once again, working with these data sources should be made asynchronous.
Let’s start with a simple example: display some data from the file system to the console output. Afterwards, we will create a simple HTTP-server and serve the contents of our file to the client. In the process, we will meet a lot of callbacks!
Create a new folder called “server-side-example”, move to it using the following command in your terminal:
cd server-side-example
and inside this directory create two files: readFile.js and text.txt.
In the text.txt file add a line of text, for example, Hello there!
, and open up the readFile.js
.
In readFile.js
add the following code:
const fs = require("fs");
const path = require("path");
fs.readFile(path.join(__dirname, "text.txt"),
{ encoding: "utf-8" }, function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
In the code above we do the following:
First, we import two modules from the Node.js standard library: the fs
("file system") module that contains methods for working with various files, and the path
module that is needed to precisely resolve directories to the files we need.
Then, we use .readFile()
method of the fs
object. This method asynchronously reads data from the file and takes three arguments: the path to the file to read (in this case, we use path.join()
method to concatenate the current directory (__dirname
) with the name of the file (text.txt
)), configuration object (in this case, just encoding), and a callback function.
According to the convention we've met above, the callback function takes two arguments: a possible error (err
) and the data (data
) from the file we want to read. In this case, we simply check for an error first, and output it the console if there was a problem (e.g. file does not exist). Otherwise, if there was no error, it means that we have some data, so we output it with console.log(data)
.
Let's launch our program with
node readFile.js
The output should be:
Hello there!
So far so good! Try and change the contents of text.txt
and restart the program. How about a wrong path for the fs.readFile()
to generate an error? Give some tweaks a shot.
Outputting to the console is great, but what about showing the contents of our fancy file via the Internet? Let’s make a super simple local HTTP server, and make it send some information from the file system to the client (i.e. the browser).
In the same folder, create a file server.js
, and open it in the editor. Add the following code:
const fs = require("fs");
const path = require("path");
const http = require("http");
const server = http.createServer(function (request, response) {
fs.readFile(
path.join(__dirname, "text.txt"),
{ encoding: "utf-8" },
function (err, data) {
if (err) {
response.write(`<h1>An error occurred!</h1>`);
response.end();
} else {
response.write(
`<h1>The following text is from the file system:</h1><p>${data}</p>`
);
response.end();
}
}
);
});
server.listen(8080, function () {
console.log("Server started on 8080");
});
Here we first import the two already familiar modules, fs
and path
, and a new module - http
that we will use to create our server using its .createServer()
method.
Let's talk about the structure of our server as a whole. We declare a variable server
and assign it to the value of http.createServer()
. This method takes a callback function as an argument, and this callback function is going to handle requests to our server. We will return to it in a second.
Then, we call the .listen()
method of our server
object to start listening for requests on one of our machine's ports. This method takes a port to listen on as the first argument, and an optional callback function: here we use it just to show that the server started successfully.
Returning back to the callback of our .createServer()
. This function takes two arguments: HTTP request and HTTP response objects, named conventionally request and response. An important note here: once again, we are defining a function to be called, not invoking it here. This function will be icalled, when our server receives an HTTP request (e.g. when we visit localhost:8080 in our browser after the server has started). In this function, we might have called request and response parameters any way we wanted: req and res, httpRequest and httpResponse, etc.
The request
object contains various information about the request we've received: HTTP method and URL, request headers, possible request body, and many others. If we needed to handle requests to different URLs or different types of requests (GET, POST, PUT, DELETE) we would run conditionals statements against the request
object to decide what to do with it. For simplicity, in our case, any HTTP request to our server will result in the same response.
The response
object contains different methods and properties that define, how to respond to the client-side request: what data and in what way to send back to the client. In this case, we will use only two methods: .write()
and .end()
.
response.write()
takes the data to write to the client in the response as the parameter. Here we can directly write HTML, and it will be interpreted as such by the browser. Using the already familiar string interpolation, we can use backticks \
and hardcode the 'static' parts of our string, and use curly braces with the $ sign ${}
to add some dynamic data to it. Here we use ${}
to insert the data from the text.txt
in our response.
response.end()
terminates the request-response cycle and signals the client-side that our response ends here.
We use these two neat methods in the callback of the fs.readFile()
. If the data has been read successfully, we send it to the client in the response, if there was an error while reading the file, we respond with an error message.
Thus, our .createServer()
works as following:
- The server receives a request, calls its handler callback;
- The handler callback calls
fs.readFile()
that asynchronously reads a file from the file system; - The callback passed to
fs.readFile()
responds to the client withresponse.write()
andresponse.end()
once the asynchronous operation completes.
Let’s see this in action! In the terminal run:
node server.js
to launch the server and check that you got
Server started on 8080
in the terminal window. Open localhost:8080 in the browser. You’re likely to see something like this:
Nice!
We've just created an HTTP server that sends dynamic data to the client. Try and change the contents of text.txt
and refresh the page. How about giving fs.readFile()
a wrong path? Don't forget to save the file and restart the server after adding changes.
Of course, in a real-world application, our code would be much more sophisticated. We would be more likely to use some sort of server-side framework within Node.js (e.g. Express.js) to handle requests to different routes, and the whole architecture would be much more complex. However, the very base of the application would be the same. And, just as we saw, it would be heavily based on callbacks.
Now let’s take a look at how we can use callbacks for asynchronous operations on the client-side. Frontend, here we go!
Client-side example
On the client-side, the role of asynchronous programming is huge. It is the base of the AJAX technology, Asynchronous JavaScript And XML (even though the name is a bit obsolete since XML is not as common as it used to be). AJAX is the major tool for creating highly dynamic client-side applications that send and receive data from the server without refreshing the whole page.
Nowadays, there are several ways to implement AJAX, including XMLHttpRequest
, window.fetch(
) and external libraries like axios. With XMLHttpRequest
being the oldest one, it is a good idea to get acquainted with it first, before moving to more modern approaches.
An XMLHttpRequest
is a JavaScript Object with several built-in methods and properties aimed at fetching some data with an on-page HTTP request to the own server or some 3d party Rest API. In a typical use case, we would usually create a function that takes different configuration options as parameters, initializes a new instance of XMLHttpRequest
with these parameters inside this function, and sends the request to the specified URL with the specified HTTP method and data (if needed). What we have to do while the data is loading (e.g. show a nice loading spinner), has loaded (e.g. display it to the screen and hide the spinner), or an error occurred (e.g. hide the spinner and show an error message) is all handled by callbacks we define. XMLHttpRequest
has a lot of parameters and interesting features apart from the ones we are going to briefly touch upon, and I would strongly recommend checking out MDN documentation and playing with some data after this tutorial.
In this example, we are going to create a web-page that loads some posts from an API on a button click, shows a loading spinner once the request started, and displays the posts to the page or shows an error message if something goes wrong. For the data source, we will use jsonplaceholder - a great tool for learning AJAX and HTTP requests in general. It contains various sets of data that imitate a typical response from a server in JSON format - blogposts, comments, users, etc. Whether you need to take a good grasp on basic frontend concepts or learn a new library (e.g. React or Vue.js) jsonplaceholder certainly worth bookmarking.
Create a new file in our folder and call it client-side-example.html
. For simplicity, we will keep our CSS, HTML, and JavaScript in the same file.
Inside our new file within the body
tags add the following code:
</main>
<h1>Browser example</h1>
<h2>Posts</h2>
<button
id="fetchPostsBtn"
>
Fetch Posts
</button>
<div id="spinner" style="display: none;">
Loading...
</div>
<div id="postsDiv">
</div>
</main>
<script>
</script>
Here we created a <main>
container for our application with three elements with the defined id
attribute that we will use in our JavaScript code: a <button>
, a <div>
that will become a spinner (but for now just says "Loading..."), and container <div>
for our posts. Within the <script>
</script>
tags we will place the code to manipulate the web-page content.
Next, between the <script>
</script>
tags add the following code:
let postsDiv = document.querySelector('#postsDiv');
let fetchPostsBtn = document.querySelector('#fetchPostsBtn');
let spinner = document.querySelector('#spinner');
We use the document.querySelector()
to find the elements we need by id and create three variables that point at these elements.
Now, we will declare a function fetchPosts()
and pass it as the callback function of the .addEventListener()
method of the fetchPostsBtn
:
function fetchPosts () {
console.log('Posts fetched!');
}
fetchPostsBtn.addEventListener('click', fetchPosts);
Right now, it does do much: it simply outputs "Posts fetched!" to the console in our browser's developer tools. Open the file client-side-example.html
with a browser, open developer tools (Ctrl + Shift + J
in most cases), and click our button a couple of times to check if it's working.
What is worth noticing here, is that the .addEventListener()
method of our button takes two parameters: the type of event to add a function to ('click', in this case) and a callback function to invoke when the event takes place. Here we defined our fetchPosts()
function separately, so we pass it as the second parameter without parentheses.
Next, we will make our fetchPosts()
function actually fetch posts from the data source. Let's fill the body of the function:
function fetchPosts () {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
console.log(xhr.response);
}
xhr.onerror = function() {
console.log('An error occurred!');
}
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
xhr.responseType = 'json';
xhr.send();
}
Here, we first create a new instance of XMLHttpRequest
Object: we call the constructor method of XMLHttpRequest with the new
keyword and assign it to the variable xhr
. Now, xhr
is a separate instance of XMLHttpRequest Object that has the attributes and methods we need to make a request.
First, let's take a look at the xhr.open()
and xhr.send()
.
xhr.open()
is the method responsible for the main configurations of the request, it takes the HTTP method as the first parameter ('GET' in this case) and the URL to make a request to ('https://jsonplaceholder.typicode.com/posts').
xhr.responseType
property defines, what type of data we expect in the response from the server. We expect JSON, so we assign it to 'json'.
xhr.send()
method actually sends the request. After the request is sent, events within the request start happening: loadstart, loadend, error, and others. On each of these events, we can define a function to invoke. Let's start with xhr.onload
and xhr.onerror
.
xhr.onload
property should be a function to invoke when the response has been successful. In this case, the response data is accessible via the xhr.response
property, that we display to the console.
xhr.onerror
function is invoked when some sort of error happens. We can define error handling logic in this function. For the sake of simplicity, we just console.log()
an error message.
Let's test our simple function. Save the file, refresh the page in the web browser, and click the button. Within a few seconds, we should see a huge array of objects in our console: this is the data we are going to display. Take a minute and have a good look at the structure of the data we've just received. What properties does each object have? Try to change the URL parameter in the xhr.open()
to some wrong URL, what will the console display now when we click the button?
Change the URL back to 'https://jsonplaceholder.typicode.com/posts' and let's move on to displaying our data on the page.
function fetchPosts () {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
let posts = xhr.response;
posts.forEach(function (post) {
let postDiv = document.createElement('div');
postDiv.className = 'postsDiv__postDiv';
let postHeader = document.createElement('h3');
postHeader.textContent = post.title;
postHeader.className = 'postsDiv__postDiv__postHeader';
let postBody = document.createElement('p');
postBody.textContent = post.body;
postBody.className = 'postsDiv__postDiv__postBody';
postDiv.appendChild(postHeader);
postDiv.appendChild(postBody);
postsDiv.appendChild(postDiv);
});
fetchPostsBtn.disabled = true;
}
xhr.onerror = function() {
alert('An error occurred!');
}
xhr.onloadstart = function() {
spinner.style.display = 'block';
}
xhr.onloadend = function() {
spinner.style.display = 'none';
}
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts');
xhr.responseType = 'json';
xhr.send();
}
Here we added logic to the xhr.onload
and xhr.onerror
methods, and added two new methods: xhr.onloadstart
and xhr.onloadend
.
In the xhr.onload
method, we first declare a variable posts
and assign it the value of xhr.response
, making it an array of objects. Then, we use Array.forEach()
method of the posts
variable, to iterate over each post in our array. In the callback function for each item in the array, we create a new HTML <div>
element, with the class of 'postsDiv__postDiv'. This will be the container for the post. After that, we create HTML elements for the post header and body (h3
and p
, respectively), and assign their .textContent
property to the value of the respective properties of the post
: post.title
and post.body
. At the end of the iteration, we append the postHeader
and postBody
to their container postDiv
, and append our postDiv
to the postsDiv
to add the newly-created element to the DOM tree. After all the iterations, we disable the fetchPostsBtn
by assigning its .disabled
property to true
.
In the xhr.onerror
method, we simply instruct the code to show a standard browser alert pop-up with a message 'An error occurred!'.
Finally, in the xhr.onloadstart
and xhr.onloadend
we show and hide the spinner
by setting its .style.display
property to 'block' when the request starts, and hiding it from the screen with .style.display
set to 'none' when the request finishes (successfully or not).
Now it is time to test our app! Save the file and refresh the tab in the browser. Click the button to load the posts. We should see something like this:
Try and change the URL to something wrong once again: after a while, a pop-up alert should tell you that some error occurred.
Nice! Our application works as intended: we asynchronously fetch posts on button click without freezing the browser and let our user know if something is going on by showing the 'Loading...' message and alerting the user if a problem took place.
As a little bonus, let's style our app a little bit, to have a fancy moving spinner and neatly-looking posts.
Change the spinner
div
in the following way:
<main>
<h1>Browser example</h1>
<h2>Posts</h2>
<button
id="fetchPostsBtn"
>
Fetch Posts
</button>
<div id="spinner" style="display: none;">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div id="postsDiv">
</div>
</main>
These new div
s are needed to create the spinner effect.
And in the head
between style
tags add the following CSS code:
/* Styling the heading */
h1 {
text-align: center;
}
h2 {
text-align: center;
}
#fetchPostsBtn {
display: block;
margin-left: auto;
margin-right: auto;
}
/* Styling the posts */
#postsDiv {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
.postsDiv__postDiv {
width: 15em;
min-height: 10em;
margin: 0.3em;
animation: postEnter 0.5s forwards;
}
.postDiv__postHeader {
text-align: center;
}
.postDiv__postBody {
text-align: justify;
}
@keyframes postEnter {
from {
opacity: 0;
transform: translate(0, 10em);
}
to {
opacity: 1;
transform: translate(0, 0);
}
}
/* Styling the spinner */
#spinner {
display: block;
position: fixed;
top: 30vh;
left: calc(50% - 20px);
width: 40px;
height: 40px;
}
#spinner div {
box-sizing: border-box;
display: block;
position: absolute;
width: 32px;
height: 32px;
margin: 4px;
border: 4px solid rgb(30, 191, 255);
border-radius: 50%;
animation: spinnerAnimation 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: rgb(30, 191, 255) transparent transparent transparent;
}
#spinner div:nth-child(1) {
animation-delay: -0.45s;
}
#spinner div:nth-child(2) {
animation-delay: -0.3s;
}
#spinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes spinnerAnimation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
Here we used some CSS animations and :nth-child()
CSS-pseudoclass to create the moving spinner effect, added an animation effect for the posts. By making the postsDiv
a flexbox with flex-wrap: wrap;
property and setting the width of the postsDiv__postDiv
class we will now have a nice grid of posts.
Save the file and refresh the tab with client-side-example.html
. We will see something like this:
Looks much more interesting! Check the code in the sandbox for reference. Try and change some CSS: maybe you want to have a different look on the button and posts? Or a fancier spinner? Check out this great resource for free pure-CSS spinners you can tweak and use in your projects.
Conclusion
Phew! It was quite a ride! Today we’ve learned a lot about asynchronous programming in JavaScript. We saw why we need asynchronous code in the first place, what kind of problems it solves, re-introduced ourselves to function declaration and invocation in JavaScript, wrote an asynchronous function of our own, and implemented server- and client-side examples of asynchronous code using callbacks.
I really hope some of the asynchronous stuff “clicked” on you after this little tutorial. If not, don’t be afraid: callbacks can be really hard to wrap your head around. Review the code you’ve just written, draw some lines and arrows in your head or on the paper: where’s the function’s declaration? where it gets called?
If you feel a runner’s high (or coder’s high?) after this tutorial: nice! Take a little break, and then open up some documentation or articles that seemed useless before, tweak our examples a little bit or write some of your own: add some new features, read the API reference of the XmlHttpRequest and Node.js fs module. There’s a lot of amazing stuff ahead like Promises and async/await. The journey is long, but you’ve just made a huge leap forward!
As a small bonus, a book I cannot recommend enough: Node.js design patterns by Mario Casciaro. Personally, I'm not a big fan of using books when it comes to learning programming languages and frameworks. This one, however, totally worth checking out. Despite the name, it's not just about design patterns or even Node.js in particular: it's a practical guide on JavaScript and designing applications as a whole. It's a tough read, but it can definitely help to take your skills to the next level.
Hope you've enjoyed this tutorial, and I'd really appreciate knowing your impressions.
Have a good one!
Top comments (0)