AJAX States

Chapter 3 13 mins

Learning outcomes:

  1. What are states of a request
  2. The readyState property
  3. The onreadystatechange event

A quick recap

In the previous chapter we saw how the whole mechanism of AJAX works and that how is a basic request composed using the open() and send() methods of the XMLHttpRequest() object.

open() was used to initialise the request whereas send() served the purpose of literally sending it to the desired location. In the discussion, however, we abstracted away certain details, specifically in the third, fourth and fifth steps.

From the point of dispatch of the request upto the point the server processes it and sends back information along with the necessary HTTP headers, the AJAX code at the client-end needs to literally track the state of the request and then the status of the data once returned by the server. This makes sure we are taking into account potential errors and problems creeping into our applications.

Didn't understand a word? Let's discuss it.

What is a state?

Say you set up an AJAX request by instantiating the XMLHttpRequest() constructor and call open() a while later. After calling the open() method you call send(), and then wait for a response to come back from the server. The response partially comes back with some headers, and then after a while with the actual data you wished for. Eventually all the data arrives at your end and the request completes.

These are the different states an AJAX request can be in, starting from the point it is created using the XMLHttpRequest() constructor.

A state is simply the condition, which the request is currently in.

For example the request can be uninitialised, in the process of loading, loaded successfully and so on.

See if you can spot all the discrete states of a request from the first paragraph in this section! It would be a good starter exercise.

The significance of these states is that we can use them to track a request and see when does it reach completion, and hence the point where we can safely extract data out of it. Without states we could simply not know when a request ends - which is essential for a smooth and error-free workflow.

So how many states does AJAX requests have and how do the states actually look? Are they numbers, booleans, objects or some strange codes?

Let's discuss all this next.

Different states of a request

At the basic level, an AJAX request can take on one of the following 5 different states:

  1. After an XMLHttpRequest() object is instantiated and before the open() method is called, the state of the AJAX request stands as 0. This implies the request as uninitialised - since initialisation is done using open() which is not called as of yet.
  2. Once open() is called, and while the send() method is not, the state of the request changes to 1 - signifying it as initialised.
  3. When send() is invoked and the headers of the response have been received the state changes to 2, implying it as interactive.
  4. Now after the headers have been received successfully, obviously the system then waits for the actual content to start flowing in. As soon as it starts to flow in, the state changes to 3, which means that the request is now loading. Usually this happens pretty quickly.
  5. Finally, when all of the content arrives successfully at the client-end, the state changes to its last value - 4 - meaning that it has been loaded.

The state 4 indicates that our request has been successfully fulfilled and it is now that we can sensibly retrieve the data of the response. Retrieval of data before this point is meaningless since the request is still on its way to completion and hence still receving the data of the response.

Usually in an AJAX application, we are specifically interested in monitoring for this state change since it directly relates to being able to extract out the data from the response.

In short, every interesting thing that happens on AJAX requests, in effect, happens at state 4.

However, take special note of the fact that this state doesn't always mean that everything went good on the backend. It simply means that we have a response ready for our request - the response itself can even be an error message!

We'll discover more to this as we learn request status in the sections below.

So these are the five states of any AJAX request from the beginning to the very end. Now a question arises that is there any way we can view these states for an AJAX request in JavaScript?

The answer is simple - yes!

The readyState property

The readyState property of the XMLHttpRequest() object, holds any one of the numbers from 0 to 4 depending on the state of the request at the instance of its call.

Following are some instances of readyState called at different places in the simple AJAX code we wrote in the previous AJAX Mechanism chapter:

var xhr = new XMLHttpRequest();

console.log(xhr.readyState); // 0
// readyState is 0 since the request is uninitialised

xhr.open("GET", "ajax.txt", true);

console.log(xhr.readyState); // 1
// readyState is 1 since now the request is initialised

As you can see from the logs above, and in accordance to what we said earlier, as soon as the XMLHttpRequest() object is instantiated (and open() is not called), its readyState property gets set to 0 - which means "uninitialised". This can be verified by line 3.

Moving on, once open() is called and the request initialisations are made (in line 6), its readyState changes to 1 - implying "initialised". This can be verified by line 8.

Now at this point there is one thing worth mentioning, relating to the code above: we only made logs for states 0 and 1 because only they can be logged directly as shown above, unlike the states 2, 3 and 4. But why?

The problem is that after state 1, a network roundtrip is involved and so all the states 2, 3 and 4 aren't guaranteed to be taken by the request at a given instance of time. For example at one instance it might be in state 3, but in another such instance of a request it might be in state 4. Sometimes the request may complete quickly while sometimes it may take time.

The way the states 0 and 1 work enables us to track them using direct logs as we did above. However for the other three states we can't do this because we don't know exactly when one will change to the other.

Following is an illustration of this problem to help you better understand it. Take note of the last three logs in the code, since the rest is essentially the same as the code shown above.

var xhr = new XMLHttpRequest();
console.log(xhr.readyState); // 0

xhr.open("GET", "ajax.txt", true);
console.log(xhr.readyState); // 1

xhr.send();
console.log(xhr.readyState); // 1
console.log(xhr.readyState); // 1
console.log(xhr.readyState); // 1

If you look closely here, you'll find certain strange things.

Firstly you might think that the three logs starting from line 8 output 1 because the response hasn't yet been received, but this is not the case at all. Instead what is happening is something really interesting and one most developers won't be aware of at all!

Once dispatched and received, any further processing of AJAX requests is delayed until the interpreter is free from executing the main script.

Let's see what this means.

Beyond this point it is assumed that you know what JavaScript's call stack and task queue are.

How send() works internally?

The interpreter comes across the send() method in line 7 and likewise invokes it. This gets the internal browser engine to dispatch the desired request and consequently exit the method. The method completes at this point and the interpreter continues on with the next instruction.

Meanwhile the request initiated by the browser in the background completes and is therefore put in the task queue. It waits here until the JavaScript call stack gets empty. The stack empties only once the entire script finishes executing. Thus any further processing of the request, after it has been received and waiting in the queue, is delayed until the completion of the ongoing script.

This is the way JavaScript handles all asynchronous operations and events. They line up in the task queue, one after another, and execute only once the main script gets executed to its entirety.

Coming back to the topic, at least now we know the reason to why all the three logs above, after send(), were equal to 1 - simply because the request was waiting in the queue, or not yet completed, until all the logs were made. Problem solved!

Now if you didn't understand all of this, or even a certain segment, don't worry; this concept is truly complicated and requires a bit of rock solid console experimenting before it can start to make sense.

However, even if all these sequence of operations didn't happen, and assuming an ideal mechanism of send() where the request is processed exactly when it completes, rather than until the script's completion; no one will ever track states 2 to 4 using direct logs. It just doesn't make sense!

Hence, where all this long conversation converges and what is the whole point of considering states of a request is the idea of events.

Events to the rescue

Events are the ultimate solution to tracking state changes of an AJAX request. Each time the state changes, i.e the value of readyState changes, an event is fired. This event can be assigned a listener to respond to the change.

It is named onreadystatechange. Its name is quite self-explanatory to understand its purpose even if you knew nothing about it at all - it fires "on readyState's change".

Anyways moving on, let's now use this event to handle state changes in our previous code and log readyState inside the handler function, all more neatly and meaningfully.

var xhr = new XMLHttpRequest();
console.log(xhr.readyState); // 0

xhr.onreadystatechange = function() {
// whenever readyState changes, execute this code
console.log(this.readyState);
}

xhr.open("GET", "ajax.txt", true);
xhr.send();

Execute this and you'll find all the five states sequentially logged in the console.

Detailing one thing about the code above, if you notice one thing you'll see that the log for state 0 is made outside the handler of onreadystatechange. The reason for doing so is because the event only fires for state changes i.e when it changes from 0 to 1, 1 to 2 and so on.

0 is the default state of a request as it gets instantiated - it doesn't come from a state change. Think on it and you'll soon realise the fact!

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage