In this chapter, you'll investigate how a web page actually works within the browser, with a special focus on how, when, and why the browser executes the JavaScript code we provide. You'll dive deep into the intricacies of the event loop and see how we can manage it. Lastly, you'll learn about the tools that TypeScript offers you. By the end of this chapter, you will be able to better manage the asynchronous nature of the execution.
In the previous chapter, you learned the fundamentals of generics and conditional types. This chapter introduces you to event loops and asynchronous behavior. However, before you proceed with learning these topics, let's have a look at a hypothetical scenario to really understand how synchronous and asynchronous executions work.
Imagine a small bank that has a single teller. His name is Tom, and he's serving clients all day. Since it's a small bank and there are few clients, there's no queue. So, when a client comes in, they get Tom's undivided attention. The client provides all the necessary paperwork, and Tom processes it. If the process needs some kind of outside input, such as from a credit bureau or the bank's back-office department, Tom submits the request, and he and the client wait for the response together. They might chat a bit, and when the response comes, Tom resumes his work. If a document needs to be printed, Tom sends it to the printer that's right on his desk, and they wait and chat. When the printing is done, Tom resumes his work. Once the work is completed, the bank has another satisfied client, and Tom continues with his day. If somebody comes while Tom is serving a client (which happens seldom), they wait until Tom is completely done with the previous client, and only then do they begin their process. Even if Tom is waiting on an external response, the other client will have to wait their turn, while Tom idly chats with the current client.
Tom effectively works synchronously and sequentially. There are lots of benefits of this approach to working, namely, Tom (and his bosses) can always tell whether he is serving a client or not, he always knows who his current client is, and he can completely forget all the data about the client as soon as the client leaves, knowing that they have been serviced completely. There are no issues with mixing up documents from different clients. Any problems are easy to diagnose and easy to fix. And since the queue never gets crowded, this setup works to everyone's satisfaction.
So far, so good. But what happens when the bank suddenly gets more clients? As more and more clients arrive, we get a long queue, and everyone is waiting, while Tom chats with the current client, waiting on a response from the credit bureau. Tom's boss is, understandably, not happy with the situation. The current system does not scale – at all. So, he wants to change the system somehow, to be able to serve more clients. How can he do that? You will look at a couple of solutions in the following section.
Basically, there are two different approaches. One is to have multiple Toms. So, every single teller will still work in the exact same simple and synchronous way as before – we just have lots of them. Of course, the boss will need to have some kind of organization to know which teller is available and which is working, whether there are separate queues for each teller, or a single large queue, along with some kind of distribution mechanism (that is, a system where a number is assigned to each customer). The boss might also get one of those big office printers, instead of having one printer per teller, and have some kind of rule in order to not mix up the print jobs. The organization will be complex, but the task of every single teller will be straightforward.
By now, you know we're not really discussing banks. This is the usual approach for server-side processing. Grossly simplified, the server process will have multiple sub-processes (called threads) that will work in parallel, and the main process will orchestrate everything. Each thread will execute synchronously, with a well-defined beginning, middle, and end. Since servers are usually machines with lots of resources, with heavy loads, this approach makes sense. It can accommodate low or high loads nicely, and the code that processes each request can be relatively simple and easy to debug. It even makes sense to have the thread wait for some external resource (a file from the file system, or data from the network or database), since we can always spin up new threads if we have more requests. This is not the case with real live tellers. We cannot just clone a new one if more clients come. The kind of waiting done by the threads (or by Tom) is usually referred to as busy waiting. The thread is not doing anything, but it's not available for any work, since it's busy doing something – it's busy waiting. Just like Tom was actually busy chatting with the client while waiting for a response from the credit bureau.
We have a system that can be massively parallel and concurrent, but still, each part of it is run synchronously. The benefit of this approach is that we can serve many, many clients at the same time. One obvious downside is the cost, both in hardware and in complexity. While we managed to keep the client processing simple, we'll need a huge infrastructure that takes care of everything else – adding tellers, removing tellers, queueing customers, managing access to the office printer, and similar tasks.
This will use all the available resources of the bank (or the server), but that is fine, since that's the whole point – to serve clients, as many and as fast as possible, and nothing else.
However, there is another approach – asynchronous execution.
The other approach, the one taken by the web and, by extension, JavaScript and TypeScript, is to use just a single thread – so Tom is still on his own. But, instead of Tom idly chatting with a waiting client, he could do something else. If a situation arises where he needs some verification from the back office, he just writes down what he was doing and how far he got on a piece of paper, gives that paper to the client, and sends them to the back of the queue. Tom is now ready to start serving the next client in line. If that client does not need external resources, they are processed completely and are free to leave. If they need something else that Tom needs to wait for, they too are sent to the back of the line. And so on, and so forth. This way, if Tom has any clients at all, he's processing their requests. He's never busy waiting, instead, he's busy working. If a client needs to wait for a response, they do so separately from Tom. The only time Tom is idle is when he has no clients at all.
The benefit of this approach is fairly obvious – before, Tom spent a lot of his time chatting, now he is working all the time (of course, this benefit is from Tom's boss' point of view – Tom liked the small talk). An additional benefit is that we know our resource consumption up front. If we only have one teller, we know the square footage that we will need for the office. However, there are some downsides as well. The most important downside is that our clients now have to know our process quite intimately. They will need to understand how to queue and requeue, how to continue working from where they left off, and so on. Tom's work also got a lot more complicated. He needs to know how to pause the processing of a client, how to continue, how to behave if an external response is not received, and so on. This model of working is usually called asynchronous and concurrent. Doing his job, Tom will jump between multiple clients at the same time. More than one client will have their process started but not finished. And there's no way for a client to estimate how long it will take to process their task once it is started – it depends on how many other clients Tom processes at the same time.
From the early days, this model made much more sense for the web. For starters, web applications are processed on the device of the client. We should not make any technical assumptions about it – as we cannot be sure about the kind of device that the client might be using. In essence, a web page is a guest on the client's device – and it should behave properly. For example, using up all of a device's resources to show what amounts to a fancy animation is not proper behavior at all. Another important issue is security. If we think of web pages as applications that contain some code, we're basically executing someone's code on our machine whenever we enter a web address in the browser's address bar.
The browser needs to make sure that the code on the page, even if it's malicious, is restricted in what it can do to our machine. The web would not have been as popular as it is today if visiting a website could make your computer explode.
So, since the browser cannot know in advance which pages it will be used for, it was decided that each web page will only get access to a single thread. Also, for security reasons, each web page will get a separate thread, so a running web page cannot meddle in the execution of other pages that may execute at the same time (with features such as web workers and Chrome applications, these restrictions are somewhat loosened, but in principle, they still apply).
There is simply no way for a web page to spawn enough threads to swarm the system, or for a web page to get the data from another web page. And, since a web page needs to do lots of things at once, using the synchronous and sequential approach was out of the question. That is why all the JavaScript execution environments completely embraced the asynchronous, concurrent approach. This was done to such an extent that some common synchronization techniques are, intentionally, just not available in JavaScript.
For example, lots of other languages have a "wait some time" primitive, or a library function that does that. For example, in the C# programming language, we can have this code:
Console.WriteLine("We will wait 10 s");
Thread.Sleep(10000);
Console.WriteLine("... 10 seconds later");
Thread.Sleep(15000);
Console.WriteLine("... 15 more seconds later");
This code will write some text to the console, and 10 seconds later, write some more text. During the 25 seconds of the wait, the thread this executes on will be completely non-responsive, but the code written is simple and linear – easily understood, easily changeable, and easily debuggable. JavaScript simply does not have such a synchronous primitive, but it has an asynchronous variant in the setTimeout function. The simplest equivalent code would be the following:
console.log("We will wait 10 s");
setTimeout(() => {
console.log("... 10 seconds later");
setTimeout(() => {
console.log("... 15 more seconds later");
}, 15000);
}, 10000);
It's obvious that this code is much more complex than the C# equivalent, but the advantage that we get is that this code is non-blocking. In the 25 total seconds that this code is executing, our web page can do everything it needs to do. It can respond to events, the images can load and display, we can resize the window, scroll the text – basically, the application will resume the normal and expected functionalities.
Note that while it's possible to block the JavaScript execution with some special synchronous code, it's not easy to do it. When it does actually happen, the browser can detect that it did happen and terminate the offending page:
When a JavaScript execution environment, such as a node or a browser loads a JavaScript file, it parses it and then runs it. All the functions that are defined in a JavaScript file are registered, and all the code that is not in a function is executed. The order of the execution is according to the code's position in the file. So, consider a file having the following code:
console.log("First");
console.log("Second");
The console will always display this:
First
Second
The order of the output cannot be changed, without changing the code itself. This is because the line with First will be executed completely – always – and then, and only then, will the line with Second begin to execute. This approach is synchronous because the execution is synchronized by the environment. We are guaranteed that the second line will not start executing, until and unless the line above it is completely done. But what happens if the line calls some function? Let's take a look at the following piece of code:
function sayHello(name){
console.log(`Hello ${name}`);
}
function first(){
second();
}
function second(){
third();
}
function third(){
sayHello("Bob");
}
first();
When the code is parsed, the environment will detect that we have four functions – first, second, third, and sayHello. It will also execute the line of code that is not inside a function (first();), and that will start the execution of the first function. But that function, while it's executing, calls the second function. The runtime will then suspend the running of the first function, remember where it was, and begin with the execution of the second function. This function, in turn, calls the third function. The same thing happens again – the runtime starts executing the third function, remembering that once that function is done, it should resume with the execution of the second function, and that once second is done, it should resume with the execution of the first function.
The structure the runtime uses to remember which function is active, and which are waiting, is called a stack, specifically, the call stack.
Note
The term "stack" is used in the sense of a stack of dishes, or a stack of pancakes. We can only add to the top, and we can only remove from the top.
The executing functions are put one on top of the other, and the topmost function is the one being actively executed, as shown in the following representation:
In the example, the third function will call the sayHello function, which will in turn call the log function of the console object. Once the log function finishes executing, the stack will start unwinding. That means that once a certain function finishes executing, it will be removed from the stack, and the function below it will be able to resume executing. So, once the sayHello function finishes executing, the third function will resume and finish in turn. This will trigger the continuation of the second function, and when that function is done as well, the first function will continue, and eventually finish. When the first function finishes executing, the stack will become empty – and the runtime will stop executing code.
It's worth noting that all of this execution is done strictly synchronously and deterministically. We can deduce the exact order and number of function calls just from looking at the code. We can also use common debugging tools such as breakpoints and stack traces.
In this exercise, we'll define few simple functions that call each other. Each of the functions will log to the console when it starts executing and when it's about to finish executing. We will analyze when and in what order the output is mapped to the console:
Note
The code files for this exercise can be found at https://packt.link/X7QZQ.
function inner () {
}
function middle () {
}
function outer () {
}
function inner () {
console.log(" Inside inner function");
}
function middle () {
console.log(" Starting middle function");
inner();
console.log(" Finishing middle function");
}
function outer () {
console.log("Starting outer function");
middle();
console.log("Finishing outer function");
}
outer();
tsc stack.ts
node stack.js
You will see the output looks like this:
Starting outer function
Starting middle function
Inside inner function
Finishing middle function
Finishing outer function
The output shows which function started executing first (outer), as that is the first message displayed. It can also be noted that the middle function finished executing after the inner function was already finished, but before the outer function was finished.
When a web page is requested by the user, the browser needs to do lots of things. We won't go into the details of each of them, but we'll take a look at how it handles our code.
First of all, the browser sends the request to the server and receives an HTML file as a response. Within that HTML file, there are embedded links to resources that are needed for the page, such as images, stylesheets, and JavaScript code. The browser then downloads those as well and applies them to the downloaded HTML. Images are displayed, elements are styled, and JavaScript files are parsed and run.
The order in which the code is executed is according to the file's order in the HTML, then according to the code's position in the file. But when are the functions called? Let's say we have the following code in our file:
function sayHello() {
console.log("Hello");
}
sayHello();
First, the sayHello function is registered, and then when it's called later, the function actually executes and writes Hello to the console. Take a look at the following code now:
function sayHello() {
console.log("Hello");
}
function sayHi() {
console.log("Hi");
}
sayHello();
sayHi();
sayHello();
When the file with the preceding code is processed, it will register that it has two functions, sayHello and sayHi. Then it will detect that it has three invocations, that is, there are three tasks that need to be processed. The environment has something that is called the task queue, where it will put all the functions that need to be executed, one by one. So, our code will be transformed into three tasks. Then, the environment will check if the stack is actually empty, and if it is, it will take the first task off the queue and start executing it. The stack will grow and shrink depending on the execution of the code of the first task, and eventually, when the first task is finished, it will be empty. So, after the first task is executed, the situation will be as follows:
Once the stack is empty, the next task is dequeued and executed, and so on, until both the task queue and the stack are empty, and all the code is executed. Again, this whole process is done synchronously, in a specified order.
Now, take a look at a different example:
function sayHello() {
console.log("Hello");
}
document.addEventListener("click", sayHello);
If you have this code in a JavaScript file that is loaded by the browser, you can see that the sayHello function is registered but not executed. However, if you click anywhere on the page, you will see that the Hello string appears on the console, meaning the sayHello function got executed. If you click multiple times, you'll get multiple instances of "Hello" on the console. And this code did not invoke the sayHello function even once; you don't have the sayHello() invocation in the code at all.
What happened is, you registered our function as an event listener. Consider that you don't call our function at all, but the browser's environment will call it for us, whenever a certain event occurs – in this case, the click event on the whole document. And since those events are generated by the user, we cannot know if and when our code will execute. Event listeners are the principal way that our code can communicate with the page that it's on, and they are called asynchronously – you don't know when or if the function will be invoked, nor how many times it will be invoked.
What the browser does, when an event occurs, is to look up its own internal table of registered event handlers. In our case, if a click event occurs anywhere on the document (that's the whole web page), the browser will see that you have registered the sayHello function to respond to it. That function will not be executed directly – instead, the browser will place an invocation of the function in the task queue. After that, the regular behavior explained previously takes effect. If the queue and stack are empty, the event handler will begin executing immediately. Otherwise, our handler will wait for its turn.
This is another core effect of asynchronous behavior – we simply cannot guarantee that the event handler will execute immediately. It might be the case that it does, but there is no way to know if the queue and stack are empty at a specific moment. If they are, we'll get immediate execution, but if they're not, we'll have to wait our turn.
Most of our interaction with the browser will be done in the same pattern – you will define a function, and pass that function as a parameter to some browser API. When and if that function will actually be scheduled for execution will depend on the particulars of that API. In the previous case, you used the event handler API, addEventListener, which takes two parameters, the name of an event, and the code that will be scheduled when that event happens.
Note
You can get a list of different possible events at https://developer.mozilla.org/en-US/docs/Web/Events.
In the rest of this chapter, you will use two other APIs as well, the environment's method to defer some code for later execution (setTimeout) and the ability to call on external resources (popularly called AJAX). There are two different AJAX implementations that we will be working with, the original XMLHttpRequest implementation, and the more modern and flexible fetch implementation.
As mentioned previously, the environment offers no possibility to pause the execution of JavaScript for a certain amount of time. However, the need to execute some code after some set amount of time has passed arises quite often. So, instead of pausing the execution, we get to do something different that has the same outcome. We get to schedule a piece of code to get executed after an amount of time has passed. To do that we use the setTimeout function. This function takes two parameters: A function that will need to be executed, and the time, in milliseconds, it should defer the execution of that function by:
setTimeout(function() {
console.log("After one second");
}, 1000);
Here it means that the anonymous function that is passed as a parameter will be executed after 1,000 milliseconds, that is, one second.
In this exercise, you'll use the setTimeout environment API call to investigate how asynchronous execution behaves and what it does:
Note
The code files for this exercise can be found at https://packt.link/W0mlS.
console.log("Printed immediately");
setTimeout(function() {
console.log("Printed after one second");
}, 1000);
setTimeout(function() {
console.log("Printed after two second");
}, 2000);
Here, instead of creating a function and giving it to the setTimeout function using its name, we have used an anonymous function that we have created in-place. We can also use arrow functions instead of functions defined with the function keyword.
tsc delays-1.ts
node delays-1.js
You will see the output looks like this:
Printed immediately
Printed after one second
Printed after two second
The second and third lines of the output should not appear immediately, but after 1 and 2 seconds respectively.
console.log("Printed immediately");
setTimeout(function() {
console.log("Printed after two second");
}, 2000);
setTimeout(function() {
console.log("Printed after one second");
}, 1000);
setTimeout(function() {
console.log("Printed after two second");
}, 2000);
setTimeout(function() {
console.log("Printed after one second");
}, 1000);
console.log("Printed immediately");
setTimeout(function() {
console.log("#1 Printed immediately?");
}, 0);
console.log("#2 Printed immediately.");
tsc delays-2.ts
node delays-2.js
You will see the output looks like this:
#2 Printed immediately.
#1 Printed immediately?;
Well, that looks unexpected. Both lines appear basically immediately, but the one that was in the setTimeout block, and was first in the code, came after the line at the bottom of the script. And we explicitly told setTimeout not to wait, that is, to wait 0 milliseconds before the code got executed.
To understand what happened, we need to go back to the call queue. When the file was loaded, the environment detected that we had two tasks that needed to be done, the call to setTimeout and the bottom call to console.log (#2). So, those two tasks were put into the task queue. Since the stack was empty at that time, the setTimeout call started executing, and #2 was left in the task queue. The environment saw that it has a zero delay, so immediately took the function (#1), and put it at the end of the task queue, after #2. So, after the setTimeout call was done, we were left with two console.log tasks in the queue, with #2 being the first, and #1 being the second.
They got executed sequentially, and on our console, we got #2 first, and #1 second.
In the early days of the web, it was not possible to get data from a server once the page was loaded. That was a huge inconvenience for developing dynamic web pages, and it was solved by the introduction of an object called XMLHttpRequest. This object enabled developers to get data from a server after the initial page load – and since loading data from a server means using an external resource, it had to be done in an asynchronous manner (even if it has XML right in the name, currently, it will mostly be used for JSON data). To use this object, you'll need to instantiate it and use a few of its properties.
To illustrate its usage, we'll try to get data about William Shakespeare from the Open Library project. The URL that we'll use to retrieve that information is https://openlibrary.org/authors/OL9388A.json, and the access method that we will use is GET, as we will only be getting data.
The data received is of a specific format, defined by Open Library, so you'll start by creating an interface for the data that you will actually use. You'll display only an image of the Bard (received as an array of photo IDs), and the name, so you can define the interface like this:
interface OpenLibraryAuthor {
personal_name: string;
photos: number[];
}
Next, create the XMLHttpRequest object, and assign it to a variable called xhr:
const xhr = new XMLHttpRequest();
Now you need to open a connection to our URL:
const url = "https://openlibrary.org/authors/OL9388A.json";
xhr.open("GET", url);
This call doesn't actually send anything, but it prepares the system for accessing the external resource. Lastly, you need to actually send the request, using the send method:
xhr.send();
Since the request is asynchronous, this call will execute and finish immediately. In order to actually process the data once this request is done, you need to add something to this object – a callback. That is a function that will not be executed by us, but by the xhr object, once some event happens. This object has several events, such as onreadystatechange, onload, onerror, ontimeout, and you can set different functions to react to different events, but in this case, you will just use the onload event. Create a function that will get the data from the response and show it on the web page where our script is running:
const showData = () => {
if (xhr.status != 200) {
console.log(`An error occured ${xhr.status}: ${xhr.statusText}`);
} else {
const response: OpenLibraryAuthor = JSON.parse(xhr.response);
const body = document.getElementsByTagName("body")[0];
const image = document.createElement("img");
image.src = `http://covers.openlibrary.org/a/id/${response.photos[0]}-M.jpg`;
body.appendChild(image);
const name = document.createElement("h1");
name.innerHTML = response.personal_name;
body.appendChild(name);
}
};
In this method, you will be using some properties of the xhr variable that was defined previously, such as status, which gives us the HTTP status code of the request, or response, which gives us the actual response. If we just call the showData method by ourselves, we'll most likely get empty fields or an error, as the response will not have finished. So, we need to give this function to the xhr object, and it will use it to call the showData back:
xhr.onload = showData;
Save this code as shakespeare.ts, compile it, and add it to an HTML page using the following:
<script src="shakespeare.js"></script>
You will get a result similar to the following:
As a TypeScript developer, you have been tasked with creating a simple page to view movie data. The web page will be simple, with a text input field and a button. When you enter the name of a movie in the search input field and press the button, general information about the movie will be displayed on the web page, along with some images relevant to the movie.
You can use The Movie Database (https://www.themoviedb.org/) as a source of general data, specifically its API. You need to issue AJAX requests using XmlHttpRequest, and use the data the site provides to format your own object. When using an API the data will rarely, if ever, be in the format we actually need. This means that you will need to use several API requests to get our data, and piecemeal construct our object. A common TypeScript approach to this issue is to use two sets of interfaces – one that exactly matches the format of the API, and one that matches the data that you use in your application. In this activity, you need to use the Api suffix to denote those interfaces that match the API format.
Another important thing to note is that this particular API does not allow completely open access. You'll need to register for an API key and then send it in each API request. In the setup code for this activity, three functions (getSearchUrl, getMovieUrl, getPeopleUrl) will be provided that will generate the correct URLs for the needed API requests, once the apiKey variable is set to the value you will receive from The Movie Database. Also provided will be the base HTML, styling, as well as the code used to actually display the data – all that is missing is the data itself.
Those resources are listed here:
The following steps should help you with the solution:
Note
The code files for this activity can be found at https://packt.link/Qo4dB.
Note
The solution to this activity can be found via this link.
We will improve this application further in Activity 10.02, Movie Browser using fetch and Promises, and Activity 10.03, Movie Browser using fetch and async/await. However, before we do that, you need to learn about Promises and async/await in TypeScript.
Using callbacks for asynchronous development gets the job done – and that is great. However, in many applications, our code needs to use external or asynchronous resources all the time. So, very quickly, we'll get to a situation where inside our callback, there is another asynchronous call, which requires a callback inside the callback, which in turn needs a callback on its own….
It was (and in some cases, it still is) not uncommon to be a dozen levels deep inside the callback hole.
In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last word was displayed:
Note
The code files for this exercise can be found at https://packt.link/zD7TT.
const numbers = ["One", "Two", "Three", "Four", "Five"];
setTimeout(function() {
console.log(numbers[0]);
}, 1000);
tsc counting-1.ts
node counting-1.js
You will see the output looks like this:
One
The line should appear 1 second after the application was run.
setTimeout(function() {
console.log(numbers[0]);
setTimeout(function() {
console.log(numbers[1]);
}, 1000);
}, 1000);
One
Two
setTimeout(function() {
console.log(numbers[0]);
setTimeout(function() {
setTimeout(function() {
console.log(numbers[2]);
}, 1000);
console.log(numbers[1]);
}, 1000);
}, 1000);
setTimeout(function() {
console.log(numbers[0]);
setTimeout(function() {
setTimeout(function() {
console.log(numbers[2]);
setTimeout(function() {
console.log(numbers[3]);
setTimeout(function() {
console.log(numbers[4]);
}, 1000);
}, 1000);
}, 1000);
console.log(numbers[1]);
}, 1000);
}, 1000);
One
Two
Three
Four
Five
In this simple example, we implemented a simple functionality – counting to five. But as you can already see, the code is becoming extremely messy. Just imagine if we needed to count to 20 instead of 5. That would be a downright unmaintainable mess. While there are ways to make this specific code look a bit better and more maintainable, in general, that's not the case. The use of callbacks is intrinsically connected with messy and hard-to-read code. And messy and hard-to-read code is the best place for bugs to hide, so callbacks do have a reputation of being the cause of difficult-to-diagnose bugs.
An additional problem with callbacks is that there cannot be a unified API across different objects. For example, we needed to explicitly know that in order to receive data using the xhr object, we need to call the send method and add a callback for the onload event. And we needed to know that in order to check whether the request was successful or not, we have to check the status property of the xhr object.
Fortunately, we can promise you that there is a better way. That way was initially done by third-party libraries, but it has proven to be so useful and so widely adopted that it was included in the JavaScript language itself. The logic behind this solution is rather simple. Each asynchronous call is basically a promise that, sometime in the future, some task will be done and some result will be acquired. As with promises in real life, we can have three different states for a promise:
And since promises are objects themselves, this means that promises can be assigned to variables, returned from functions, passed as arguments into functions, and lots of other things we're able to do with regular objects.
Another great feature of promises is that it is relatively easy to write a promisified wrapper around an existing callback-based function. Let's try to promisify the Shakespeare example. We'll start by taking a look at the showData function. This function needs to do a lot of things, and those things are sometimes not connected to one another. It needs to both process the xhr variable to extract the data, and it needs to know what to do with that data. So, if the API we're using changes, we'll need to change our function. If the structure of our web page changes, that is, if we need to display a div instead of an h1 element, we'll need to change our function. If we need to use the author data for something else, we'll also need to change our function. Basically, if anything needs to happen to the response, it needs to happen then and there. We have no way to somehow defer that decision to another piece of code. This creates unnecessary coupling inside our code, which makes it harder to maintain.
Let's change that. We can create a new function that will return a promise, which will provide the data about the author. It will have no idea what that data will be used for:
const getShakespeareData = () => {
const result = new Promise<OpenLibraryAuthor>((resolve, reject) => {
const xhr = new XMLHttpRequest();
const url = "https://openlibrary.org/authors/OL9388A.json";
xhr.open("GET", url);
xhr.send();
xhr.onload = () => {
if (xhr.status != 200) {
reject({
error: xhr.status,
message: xhr.statusText
})
} else {
const response: OpenLibraryAuthor = JSON.parse(xhr.response);
resolve(response);
}
}
});
return result;
};
This function returns a Promise object, which was created using the Promise constructor. This constructor takes a single argument, which is a function. That function takes two arguments as well (also functions), which are by convention called resolve and reject. You can see that the function inside the constructor just creates an xhr object, calls its open and send methods, sets its onload property, and returns. So, basically, nothing gets done, except that the request is fired off.
A promise thus created will be in the pending state. And the promise stays in this state until one of the resolve or reject functions is called inside the body. If the reject function is called, it will transition to a rejected state, and we'll be able to use the catch method of the Promise object to handle the error, and if the resolve function is called, it will transition to the resolved state, and we'll be able to use the then method of the Promise object.
One thing that we should note is that this method does nothing that is UI-related. It does not print any errors on the console or change any DOM elements. It simply promises us that it will get us an OpenLibraryAuthor object. Now, we're free to use this object however we want:
getShakespeareData()
.then(author => {
const body = document.getElementsByTagName("body")[0];
const image = document.createElement("img");
image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;
body.appendChild(image);
const name = document.createElement("h1");
name.innerHTML = author.personal_name;
body.appendChild(name);
})
.catch(error => {
console.log(`An error occured ${error.error}: ${error.message}`);
})
In this piece of code, we call the getShakespeareData data function, and then on its result, we're calling two methods, then and catch. The then method only executes if the promise is in the resolved state and it takes in a function that will get the result. The catch method only executes if the promise is in the rejected state, and it will get the error as an argument to its function.
One important note for the then and catch methods – they also return promises. This means that Promise objects are chainable, so instead of going in depth, as we did with callbacks, we can go in length, so to say. To illustrate that point, let's count to five once again.
Note
A more comprehensive discussion of Promises will be presented in Chapter 12, Guide to Promises in TypeScript.
In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last one was displayed:
Note
The code files for this exercise can be found at https://packt.link/nlge8.
const numbers = ["One", "Two", "Three", "Four", "Five"];
const delay = (ms: number) => {
const result = new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms)
});
return result;
}
Since our promise will not return any meaningful result, instead just resolving after a given amount of milliseconds, we have provided void as its type.
delay(1000)
.then(() => console.log(numbers[0]))
tsc counting-2.ts
node counting-2.js
You will see the output looks like this:
One
The line should appear 1 second after the application was run.
delay(1000)
.then(() => console.log(numbers[0]))
.then(() => delay(1000))
We can do this because the result of the then method is Promise, which has its own then method.
delay(1000)
.then(() => console.log(numbers[0]))
.then(() => delay(1000))
.then(() => console.log(numbers[1]))
delay(1000)
.then(() => console.log(numbers[0]))
.then(() => delay(1000))
.then(() => console.log(numbers[1]))
.then(() => delay(1000))
.then(() => console.log(numbers[2]))
.then(() => delay(1000))
.then(() => console.log(numbers[3]))
.then(() => delay(1000))
.then(() => console.log(numbers[4]))
Let's compare this code with the code of the previous exercise. It's not the cleanest code, but its function is relatively obvious. We can see how we could expand this code to count to 20. And the major benefit here is that this code, while asynchronous, is still sequential. We can reason about it, and the lines that are at the top will execute before the lines at the bottom. Furthermore, since we have objects now, we can even refactor this code into an even simpler and more extensible format – we can use a for loop.
let promise = Promise.resolve();
for (const number of numbers) {
promise = promise
.then(() => delay(1000))
.then(() => console.log(number))
};}
One
Two
Three
Four
Five
In this activity, we will be repeating the previous activity. The major difference is that, instead of using XMLHttpRequest and its onload method, we'll be using the fetch web API. In contrast to the XMLHttpRequest class, the fetch web API returns a Promise object, so instead of nesting our callbacks to have multiple API calls, we can have a chain of promise resolutions that will be far easier to understand.
The activity has the same files and resources as the previous activity.
The following steps should help you with the solution:
Although we used fetch and promises in this activity, and our code is now much more efficient but complex, the basic function of the website will be the same and you should see an output similar to the previous activity.
Note
The code files for this activity can be found at https://packt.link/IeDTF. The solution to this activity can be found via this link.
Promises solved the problem of callbacks quite nicely. However, often, they carry with them lots of unneeded fluff. We need to write lots of then calls, and we need to be careful not to forget to close any parentheses.
The next step is to add a piece of syntactic sugar to our TypeScript skills. Unlike the other things in this chapter, this feature originated in TypeScript, and was later adopted in JavaScript as well. I'm talking about the async/await keywords. These are two separate keywords, but they are always used together, so the whole feature became known as async/await.
What we do is we can add the async modifier to a certain function, and then, in that function, we can use the await modifier to execute promises easily. Let's go once more to our Shakespearean example, and let's wrap the code we used to call getShakespeareData inside another function, simply called run:
function run() {
getShakespeareData()
.then(author => {
const body = document.getElementsByTagName("body")[0];
const image = document.createElement("img");
image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;
body.appendChild(image);
const name = document.createElement("h1");
name.innerHTML = author.personal_name;
body.appendChild(name);
})
.catch(error => {
console.log(`An error occured ${error.error}: ${error.message}`);
})
}
run();
This code is functionally equivalent to the code we had previously. But now, we have a function that we can mark as an async function, like this:
async function run() {
Now, we're allowed to just get the result of a promise and put it inside of a variable. So, the whole then invocation will become this:
const author = await getShakespeareData();
const body = document.getElementsByTagName("body")[0];
const image = document.createElement("img");
image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;
body.appendChild(image);
const name = document.createElement("h1");
name.innerHTML = author.personal_name;
body.appendChild(name);
You can see that we don't have any wrapping function calls anymore. The catch invocation can be replaced with a simple try/catch construct, and the final version of the run function will look like this:
async function run () {
try {
const author = await getShakespeareData();
const body = document.getElementsByTagName("body")[0];
const image = document.createElement("img");
image.src = `http://covers.openlibrary.org/a/id/${author.photos[0]}-M.jpg`;
body.appendChild(image);
const name = document.createElement("h1");
name.innerHTML = author.personal_name;
body.appendChild(name);
} catch (error) {
console.log(`An error occured ${error.error}: ${error.message}`);
}
}
You will notice that the amount of code that is deeply nested is drastically reduced. Now we can look at the code, and have a good idea of what it does, just from a quick glance. This is still the same, deeply asynchronous code, the only difference is that it looks almost synchronous and definitely sequential.
In this exercise, we'll create a function that, when executed, will output the English words one through five. Each word will appear on the screen 1 second after the last one was displayed:
Note
The code files for this exercise can be found at https://packt.link/TaH6b.
const numbers = ["One", "Two", "Three", "Four", "Five"];
const delay = (ms: number) => {
const result = new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms)
});
return result;
}
Since our promise will not return any meaningful results, instead of just resolving after a given number of milliseconds, we have provided void as its type.
async function countNumbers() {
}
countNumbers();
async function countNumbers() {
await delay(1000);
console.log(numbers[0]);
}
tsc counting-3.ts
node counting-3.js
You will see the output looks like this:
One
The line should appear 1 second after the application was run.
async function countNumbers() {
await delay(1000);
console.log(numbers[0]);
await delay(1000);
console.log(numbers[1]);
await delay(1000);
console.log(numbers[2]);
await delay(1000);
console.log(numbers[3]);
await delay(1000);
console.log(numbers[4]);
}
Since the code is completely identical for all the numbers, it's trivial to replace it with a for loop.
for (const number of numbers) {
await delay(1000);
console.log(number);
};
One
Two
Three
Four
Five
In this activity, we will be improving on the previous activity. The major difference is that instead of using the then method of the Promises class, we'll use the await keyword to do that for us magically. Instead of a chain of then calls, we'll just have code that looks completely regular, with some await statements peppered throughout.
The activity has the same files and resources as the previous activity.
The following steps should help you with the solution:
Although we used fetch and async/await in this activity, and our code is now just as efficient but less complex compared with the previous activity, the basic function of the website will be the same and you should see an output similar to the previous activity.
Note
The code files for this activity can be found at https://packt.link/fExtR. The solution to this activity can be found via this link.
In this chapter, we looked at the execution model that is used on the web, and how we can use it to actually execute code. We glanced at the surface of the intricacies of asynchronous development – and how we can use it to load data from external resources. We showed the problems that arise when we get too deep into the hole of callbacks and managed to exit it using promises. Finally, we were able to await our asynchronous code, and have the best of both words – code that looks like it's synchronous, but that executes asynchronously.
We also tested the skills developed in the chapter by creating a movie data viewer browser, first using XHR and callbacks, and then improved it progressively using fetch and promises, and then using fetch and async/await.
The next chapter will teach you about higher-order functions and callbacks.
44.192.132.66