What Are Asynchronous Requests?
Imagine you're at a restaurant. You give your order to the waiter (the request) and then you wait. But you don't just stare at the waiter. You talk to your friends or check your phone. When the food is ready (the response), the waiter brings it to you.
This is asynchronous. You make a request and continue doing other things until the response arrives. In JavaScript, this means your code can request data from a server without freezing the entire webpage. This is crucial for creating smooth user experiences.
The Old Way: XMLHttpRequest (XHR)
For many years, XMLHttpRequest was the primary way to make web requests. While it's more verbose than modern alternatives, you'll still see it in older codebases. It works by listening for changes in the request's state.
Here's how to fetch data from a public API using XHR:
JavaScript
// Create a new XHR object
const xhr = new XMLHttpRequest();
// Define what happens on state change
xhr.onreadystatechange = function() {
  // Check if the request is finished (readyState 4) and successful (status 200)
  if (this.readyState == 4 && this.status == 200) {
    // The response is text, so we parse it into a JavaScript object
    const data = JSON.parse(this.responseText);
    console.log("Data from XHR:", data);
  }
};
// Configure the request: method ('GET') and URL
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1", true);
// Send the request
xhr.send();
The main takeaway here is its event-based approach using onreadystatechange. It works, but can lead to complex nested callbacks ("callback hell").
The New Way: The Fetch API
The Fetch API is the modern, powerful replacement for XHR. It's built on Promises, which allow for cleaner, more readable asynchronous code.
A simple GET request with fetch looks like this:
JavaScript
fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => {
    // The first .then() handles the initial response from the server.
    // We need to check if the request was successful.
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    // response.json() is a method that also returns a promise.
    // It parses the response body as JSON.
    return response.json();
  })
  .then(data => {
    // This .then() receives the parsed JSON data.
    console.log("Data from Fetch:", data);
  })
  .catch(error => {
    // The .catch() block will run if any part of the chain fails.
    console.error('There was a problem with the fetch operation:', error);
  });
Notice how much cleaner this is? The .then() blocks chain together neatly, and a single .catch() handles all potential errors.
Working with JSON
JSON (JavaScript Object Notation) is the language of web APIs. It's a lightweight format for storing and transporting data that is easy for both humans and machines to read.
- JSON.stringify(): Converts a JavaScript object into a JSON string. You need this to send data to a server.
- JSON.parse(): Converts a JSON string (received from a server) into a JavaScript object.
Let's see how to POST (send) data using fetch:
JavaScript
// The data we want to send
const newPost = {
  title: 'foo',
  body: 'bar',
  userId: 1,
};
fetch('https://jsonplaceholder.typicode.com/posts', {
  // 1. Method: We're sending data, so we use 'POST'
  method: 'POST',
  // 2. Headers: Tell the server we're sending JSON
  headers: {
    'Content-Type': 'application/json',
  },
  // 3. Body: The actual data, converted to a JSON string
  body: JSON.stringify(newPost),
})
.then(response => response.json())
.then(data => {
  console.log('Server response:', data);
})
.catch(error => console.error('Error:', error));