Fetch API: The Modern Way to Request Data
In the world of modern web development, applications are rarely static. They are alive with data fetched from servers, updated in real-time, and sent back to be stored. The backbone of this communication is the Fetch API, a powerful, Promise-based interface built directly into the browser (and available in environments like Node.js) for making network requests.
It's the modern successor to the older `XMLHttpRequest` (XHR), offering a cleaner, more flexible, and more powerful API.
Part 1: The Basic GET Request & Promise Chain
The simplest `fetch()` call takes one argument: the URL of the resource you want to retrieve. It immediately returns a **Promise**. A Promise is a placeholder object for a future value—in this case, the server's response.
We handle this Promise by chaining `.then()` methods:
- The first `.then()`: Receives a `Response` object. This object doesn't contain your data directly; it's a wrapper for the entire HTTP response. You must use a method like `.json()` to parse the response body.
- The `.json()` method: This also returns a Promise, which resolves with the actual data, parsed as a JavaScript object.
- The second `.then()`: Receives the fully parsed data, ready for you to use.
- The `.catch()` block: This is crucial. It only triggers on a **network failure** (e.g., user is offline, server can't be reached).
const API_URL = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(API_URL)
.then(response => {
// We'll add an error check here later
return response.json();
})
.then(data => {
console.log(data); // { userId: 1, id: 1, title: '...', completed: false }
})
.catch(error => {
console.error('Fetch Error:', error); // Only catches network failures
});The `fetch()` Error Handling Quirk
This is the single most important concept to understand about `fetch()`: **It does NOT reject on HTTP error statuses like 404 or 500.**
A `fetch()` Promise only rejects if there's a network-level failure. If you request a page that doesn't exist (a 404), as far as `fetch()` is concerned, the request was "successful" because the server *did* send a response (the 404 page).
To handle this, you must manually check the `response.ok` property:
❌ Bad Practice
fetch('api/does-not-exist')
.then(res => res.json())
.then(data => ...)
.catch(err => {
// This will NOT run on a 404!
});✔️ Good Practice
fetch('api/does-not-exist')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => ...)
.catch(err => {
// This WILL now catch the error!
});Part 2: The `async/await` Syntax
Promise chains are powerful, but they can be hard to read. Modern JavaScript (ES2017+) introduced `async/await` syntax, which is just "syntactic sugar" on top of Promises. It lets you write asynchronous code that *looks* synchronous.
- `async` function: You declare a function with the `async` keyword. This automatically makes it return a Promise.
- `await` keyword: Inside an `async` function, you can use `await` to "pause" the function's execution until a Promise settles.
- Error Handling: You use a standard `try...catch` block, which is much more intuitive.
const getTodo = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
};
getTodo();Part 3: Sending Data (POST, PUT, DELETE)
`fetch()` isn't just for getting data. To send data, you pass a second argument: an **options object**.
const newTodo = {
userId: 10,
title: 'Learn Fetch API',
completed: false
};
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST', // Specify the method
headers: {
'Content-Type': 'application/json' // Tell the server we're sending JSON
},
body: JSON.stringify(newTodo) // Convert the JS object to a JSON string
})
.then(response => response.json())
.then(data => {
console.log('Success:', data); // Logs the new post (with id) from the server
})
.catch(error => {
console.error('Error:', error);
});Part 4: Aborting a Request
What if a user clicks away before a large request finishes? You can abort a `fetch()` request using an `AbortController`.
// Create a controller
const controller = new AbortController();
const signal = controller.signal;
// Pass the signal to fetch
fetch('https/api.example.com/very-large-file', { signal })
.then(...)
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
}
});
// To abort:
controller.abort();Key Takeaway: The Fetch API is the modern standard for network requests. Master the `async/await` syntax, and **always remember to manually check for `response.ok`** to handle HTTP errors correctly.