Skip to content

AJAX Events

Handling Asynchronous Operations: Promises and Async/Await

Section titled “Handling Asynchronous Operations: Promises and Async/Await”

Modern JavaScript relies heavily on Promises and the async/await syntax to manage asynchronous operations like network requests (Fetch API), timers, and more. This provides a cleaner and more manageable alternative to older callback-based patterns.

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value.

A Promise can be in one of three states:

  • Pending: Initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully, and the Promise has a resulting value.
  • Rejected: The operation failed, and the Promise has a reason for the failure (an error).

The Fetch API returns Promises. You interact with them using the .then(), .catch(), and .finally() methods.

  • .then(onFulfilled, onRejected): Attaches callbacks for the fulfilled and rejected states. You typically use .then(onFulfilled) for success and chain a .catch(onRejected) for errors.

  • .catch(onRejected): Attaches a callback only for the rejected state. Catches errors from the preceding Promise or any .then() callbacks in the chain.

  • .finally(onFinally): Attaches a callback that executes regardless of whether the Promise was fulfilled or rejected (e.g., for cleanup).

Example using Promises with Fetch:
console.log('Starting fetch...');
fetch('https://api.example.com/data')
.then(response => {
console.log('Received response object.');
if (!response.ok) {
// If status code is not OK, create an error to be caught by .catch()
throw new Error(`Network response was not ok: ${response.status}`);
}
return response.json(); // Returns another Promise
})
.then(data => {
console.log('Data parsed successfully:', data);
// Update UI with data
document.getElementById('content').innerText = JSON.stringify(data, null, 2);
})
.catch(error => {
console.error('Fetch operation failed:', error);
// Display error message
document.getElementById('content').innerText = `Error: ${error.message}`;
})
.finally(() => {
console.log('Fetch attempt finished (either success or failure).');
// Hide loading indicator, etc.
});
console.log('Fetch initiated, but code continues executing...');

Notice how .then() handlers are chained. Each .then() receives the result of the previous Promise or callback.

async/await is syntactic sugar built on top of Promises, allowing you to write asynchronous code that looks and behaves more like synchronous code, making it easier to read and maintain.

  • async function: Declaring a function with the async keyword means it will automatically return a Promise. The resolved value of the Promise will be whatever the function returns.

  • await: The await operator can only be used inside an async function. It pauses the execution of the async function until the Promise it’s waiting for is settled (either fulfilled or rejected). If fulfilled, it returns the resolved value. If rejected, it throws the error.

The previous example rewritten using `async/await`:
async function fetchData() {
const contentElement = document.getElementById('content');
console.log('Starting fetch...');
try {
const response = await fetch('https://api.example.com/data');
console.log('Received response object.');
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`);
}
const data = await response.json(); // Wait for JSON parsing
console.log('Data parsed successfully:', data);
// Update UI with data
contentElement.innerText = JSON.stringify(data, null, 2);
} catch (error) {
// Errors from fetch() or thrown manually are caught here
console.error('Fetch operation failed:', error);
contentElement.innerText = `Error: ${error.message}`;
} finally {
console.log('Fetch attempt finished (either success or failure).');
// Hide loading indicator, etc.
}
}
console.log('Calling fetchData...');
fetchData(); // Call the async function
console.log('fetchData called, but code continues executing...');

Key advantages of async/await:

  • Readability: Looks much cleaner than nested .then() chains.
  • Error Handling: Standard try...catch blocks can be used for handling errors from awaited Promises.
  • Debugging: Stepping through await calls in debuggers is often more straightforward.

Legacy: XMLHttpRequest and onreadystatechange

Section titled “Legacy: XMLHttpRequest and onreadystatechange”

Before Fetch and Promises, XMLHttpRequest (XHR) was the primary way to make asynchronous requests. It uses an event-based approach.

The onreadystatechange event handler is called multiple times during the request lifecycle, indicating changes in the readyState property.

PropertyDescription
onreadystatechangeStores a function to be called whenever the readyState property changes.
readyStateHolds the status of the XMLHttpRequest. Changes from 0 to 4:
0: request not initialized
1: server connection established
2: request received
3: processing request
4: request finished and response is ready
statusHolds the HTTP status code (e.g., 200 for “OK”, 404 for “Not Found”) after the request is complete (readyState is 4).

A typical XHR pattern involved checking if readyState is 4 and status is 200 inside the onreadystatechange handler:

// --- LEGACY EXAMPLE --- Not recommended for new code ---
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
// Check if the request is complete and was successful
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Success!
console.log('XHR Success:', xhr.responseText);
document.getElementById('myDiv').innerHTML = xhr.responseText;
} else {
// Error (e.g., 404, 500)
console.error('XHR Error:', xhr.status, xhr.statusText);
document.getElementById('myDiv').innerHTML = 'Error loading data.';
}
}
};
// Configure the request (GET request to 'data.txt')
xhr.open('GET', 'data.txt', true); // true makes it asynchronous
// Optional: Handle network errors
xhr.onerror = function() {
console.error('XHR Network Error occurred');
};
// Send the request
xhr.send();

While you might encounter XHR in older codebases, the Fetch API with Promises or async/await is the standard for new development due to its cleaner syntax and better integration with modern JavaScript features.