Understanding the AJAX lifecycle helps you write better asynchronous code and debug issues more effectively. Here is the complete flow from user interaction to DOM update:
XMLHttpRequest object or calls the fetch() function..then(), or await expression receives the response data.// Step 1: User event - button click
document.getElementById('load-btn').addEventListener('click', function () {
// Step 2: Show a loading indicator
document.getElementById('result').textContent = 'Loading...';
// Step 3: Create and send the request
fetch('/api/data')
// Step 6: Receive and parse the response
.then(response => {
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
return response.json();
})
// Step 7: Update the DOM
.then(data => {
document.getElementById('result').innerHTML =
`<p>Received: ${data.message}</p>`;
})
.catch(error => {
document.getElementById('result').textContent = `Error: ${error.message}`;
});
});
Synchronous code executes line by line. Each line must finish before the next one starts. If a network request takes 3 seconds, the entire browser tab freezes for 3 seconds - the user cannot click, scroll, or type.
Asynchronous code allows the browser to continue executing other code while waiting for a slow operation (like a network request) to complete. When the operation finishes, a callback or Promise resolves and handles the result.
// ---- SYNCHRONOUS (blocks the thread) ----
// XMLHttpRequest with async=false - NEVER do this in production
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', false); // false = synchronous
xhr.send();
// Browser is FROZEN here until the request completes
console.log('This runs only after the request finishes');
// ---- ASYNCHRONOUS (non-blocking) ----
// Using fetch - the browser stays responsive
console.log('1. Before fetch');
fetch('/api/data')
.then(res => res.json())
.then(data => {
console.log('3. Data received:', data); // runs later
});
console.log('2. After fetch call'); // runs immediately, before data arrives
// Output order: 1 -> 2 -> 3
JavaScript is single-threaded - it can only do one thing at a time. The event loop is the mechanism that makes asynchronous code possible without multiple threads.
fetch, setTimeout) that handle async operations outside the call stack..then(), await) go here and are processed before the regular task queue.console.log('A'); // synchronous - runs first
setTimeout(() => {
console.log('B'); // macro-task - runs last
}, 0);
Promise.resolve().then(() => {
console.log('C'); // micro-task - runs before setTimeout
});
console.log('D'); // synchronous - runs second
// Output: A -> D -> C -> B
// Explanation:
// 1. Synchronous code runs: A, D
// 2. Microtask queue drains: C (Promise callbacks)
// 3. Macro-task queue: B (setTimeout)
Explore 500+ free tutorials across 20+ languages and frameworks.