#100DaysOfMERN - Day 9

#100DaysOfMERN - Day 9

·

14 min read

✏ The Evolution of Network Requests with JavaScript

There's a number of ways how to get data from a server through a network request with JavaScript. Each of these ways uses completely different patterns and syntax, and it can be overwhelming or downright confusing for a beginner.

If you've been coding in JavaScript for years already, and watched the evolution from XMLHttpRequest => Fetch API/Promises => Async/Await, you probably felt like each of those steps made your life easier. For a beginner however, syntactic sugar is sometimes syntactic fog.

Another issue with many tutorials covering async JavaScript, even from authors who are known for their excellent teaching methods, is that they often use "fake" data (a locally stored JSON file), "fake" responses (manually setting a Boolean to indicate either success or failure of the request) and "fake" delays. I've spent ages looking for a tutorial that introduces me to network requests without the use of setTimeout, and without just logging the data in the console, instead of actually doing something with it.

Starting with this post, I'll explore all different ways to make a network request, and I'll use the jsonplaceholder.typicode.com API. It provides data for testing, for example you can fetch a list of 10 fictional user objects with ids, names, addresses, etc.


✏ XMLHttpRequest

I find this to be by far the easiest method, which is due to the fact that I'm not terribly experienced with network requests. I prefer this method for the same reasons that someone who got just introduced to Array.prototype.map prefers a for loop. You have to go through the basics first, before you can appreciate the shortcut.

The (perceived) beauty of this method is that each step is crystal clear, even if you've never done a network request before, and you're less likely to run into weird errors that you can't debug right away.

Jumping into the code:

/* the URL where I want to get data from */
const url = 'http://jsonplaceholder.typicode.com/users/';

/* creating the xhr object to interact with the server */
const xhr = new XMLHttpRequest();

Inspecting that object in the console shows a list of useful properties, methods, and events that I can attach to it. I'll go through the most important ones:

xhr.open()

To specify the request I'd like to make, I can pass in a few parameters, some optional:

  • method: find a full list of methods here, but the most common ones are 'GET' (to retrieve data from a server) and 'POST' (to submit data to a server). I only want to read data, so I'll use 'GET'

  • url: should be self-explanatory. If I want to send a request, I should probably specify where I'd like to send it to

  • async: a Boolean to indicate if the request should be made asynchronously (=true) or synchronously(=false). If omitted, it defaults to true.

So lets use the method on the request object:

/* initialising the request */
xhr.open('GET', url, true);

If you log/inspect the object before and after using the .open method, you'll see that the value of readyState has changed from 0 to 1.

xhr.readyState and onreadystatechange

xhr.readyState is a number that indicates the current state of the request. It can be one of the following:

  • 0 - UNSENT: The xhr object has been created, but no request has been made so far
  • 1- OPENED: .open was called
  • 2- HEADERS_RECEIVED: .send was called, and headers and status are available
  • 3- LOADING: Started downloading
  • 4- DONE: The operation is complete

To keep track of what's happening when, I'll use the event handler to inform me about each step:

xhr.onreadystatechange = e => console.log(e.target);

xhr.send()

There's only one final step needed - send the request. If you're only reading information, you call .send() without arguments. If you were to write on the server with a PUT request, you can pass in the data here.

xhr.send();

The communication with the server is hereby done, everything else is processing the response (and catching errors). If you inspect the xhr object, you'll see that it has a property response, holding the data you've requested.


✏ Understanding the async nature of a request

If you've only written synchronous code so far, it might take some time to get used to handling asynchronous operations. For example, the following (naive) approach might look valid to the untrained eye, but won't lead to the expected results:

const url = 'http://jsonplaceholder.typicode.com/users/';

const xhr = new XMLHttpRequest();

xhr.open('GET', url, true);
xhr.send();

const myData = xhr.response;

console.log(myData); // empty string

myData isn't undefined, because the xhr object exists, and it also has a response property, but its value is initially an empty string. Only after the data has arrived, the empty string will be overwritten with the actual response text. As the code in the above script makes an asynchronous request, the rest of the script will execute meanwhile. It won't wait for the data to arrive.

If you want to make a synchronous request, you'd have to specify that by passing in false to the .open method:

xhr.open('GET', url, false);

This way, the script will wait until the download is complete, and you'd have the response available in myData, instead of an empty string. However, it is strongly advised not to do so, and the option is already deprecated in many browsers (except if you use a web worker for the task). The reason is obvious - depending on your connection and the current well-being of the server you're connecting to, the network request might take an unknown amount of time, or fail altogether, and your script will be completely blocked.


✏ Using callbacks

The definition of a callback function is a bit unclear and it depends on who you ask. Sometimes, a callback is just generally a function that gets called by another function. In a narrower sense, a callback is a function that will be called after an ansynchronous operation has completed.

It doesn't really matter which definition you prefer, the point is that a callback function provides some code that you want to run if or as soon as something happens.

This is very clear for click event handler callbacks. You have no idea if or when a user clicks on a button, but in case they do, you want to provide the code that should run then, maybe pop up a modal or whatever, in the form of a callback function:

myButton.onclick = buttonCallback;

function buttonCallback(){
    // do stuff
}

A network request is no different. In the event that the data download is complete, you want to do something with it. There's a few events available:


✏ Detecting a successful download

The load orloadend listeners fire when the operation is complete. If something goes wrong, though, you'll have no information. The events just won't fire, presumably leaving the user staring at a white box with no content or an infinitely running spinner until they rage-close your page.

A better approach that I've seen many times (and that is supported in all browsers) is to use readystatechange, because it gives you a lot more information.

You only want to process your data if the operation is complete, so you can check on each readystatechange if it's === 4. Note that this doesn't necessarily mean that the download was successful. It only means that there's been a response, but that response could as well be a 404 not found error. You can read the server HTTP status code from xhr.status and adjust your script accordingly to handle errors as detailed as you like.

A status code of 200 is the default response for OK, in which case you can be safe that your data has arrived and is ready to be processed. Client and server errors have status codes 4xx or 5xx, and you'd definitely want to handle those, before your script kills itself.


✏ The onerror event

It's important to understand that you can't use the onerror event to listen for server errors. It won't fire if your request receives a 404 response, it will only fire if there's something wrong with your script - for example, if you mistyped the request method:

xhr.open('GIT', url, true); //  onerror event will fire

A typo in your url, however, will lead to a 404 not found server error.

✏ A network request like the ancients did it

A typical way to make a request to a server and processing it with readystatechange would be:

const url = 'https://jsonplaceholder.typicode.com/users/';
const xhr = new XMLHttpRequest();

xhr.onreadystatechange = handleData;

xhr.open('GET', url, true);
xhr.send();

function handleData(e){
    const readyState = e.target.readyState;

    if (readyState === 4){
        const status = e.target.status;

        if (status === 200){
            const myData = JSON.parse(e.target.response);
            insertDataIntoDOM(myData)

        } else if (status >= 400){
            console.log(`something went wrong :-( with error code: ${status}`)
        }
    }
}

✏ Recap

I've learned

  • how to fetch data from an API using XMLHttpRequest

✏ Next:

  • fetching data with fetch() and .then syntax
  • promises
  • some more error handling

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js