Master the JavaScript Fetch API with real-world examples, async/await, React integration, and best practices for modern web apps.
If you’re building modern web applications with React, plain JavaScript, or even TypeScript, handling HTTP requests is inevitable. Whether you’re fetching data from a REST API, submitting a form, or integrating with a third-party service, knowing how to use the fetch
API efficiently is essential. The Fetch API is a modern, promise-based way to make HTTP requests, and it’s widely supported across browsers.
Many developers rely on Axios or other libraries, but the native Fetch API is powerful and capable enough for most needs. Learning how to harness its capabilities directly gives you a lighter, more performant app without extra dependencies.
This guide will walk you through practical, real-world use cases of the Fetch API, from simple GET requests to advanced use with async/await, error handling, and integration with frontend frameworks like React.
Let’s begin with a basic example: fetching a list of posts from a placeholder API.
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error fetching posts:', error));
This code fetches posts and logs them to the console. Notice how it chains promises to handle asynchronous flow. fetch
returns a Promise
that resolves to a Response
object, and response.json()
returns another promise.
async function getPosts() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
console.log(posts);
} catch (error) {
console.error('Error fetching posts:', error);
}
}
getPosts();
Async/await makes asynchronous code look synchronous and improves readability, especially in complex flows.
async function createPost(post) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(post),
});
const data = await response.json();
console.log('Created Post:', data);
}
createPost({ title: 'New Post', body: 'Hello world', userId: 1 });
This is how you send data using the POST method. Always include the Content-Type
header when sending JSON.
async function updatePost(id, updatedFields) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedFields),
});
const data = await response.json();
console.log('Updated Post:', data);
}
updatePost(1, { title: 'Updated Title' });
PATCH allows partial updates, while PUT requires replacing the entire object.
async function deletePost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'DELETE',
});
if (response.ok) {
console.log(`Post ${id} deleted successfully.`);
} else {
console.error('Failed to delete post');
}
}
deletePost(1);
import React, { useEffect, useState } from 'react';
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
setPosts(data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
fetchData();
}, []);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
This example shows how to use Fetch in a useEffect
hook to load data when the component mounts.
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Fetch timed out');
} else {
console.error('Fetch error:', error);
}
}
}
fetchWithTimeout('https://jsonplaceholder.typicode.com/posts');
Using AbortController
is essential for handling timeouts and cancellations.
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
return await response.json();
} catch (err) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1})`);
} else {
throw err;
}
}
}
}
Retries are useful for transient errors like network failures.
response.ok
: Don’t assume the response was successful just because it resolved.try...catch
around fetch
**: Fetch only throws for network errors, not HTTP errors.AbortController
to cancel requests.The Fetch API is a fundamental tool for frontend developers working with Web APIs. From basic requests to advanced patterns like retries and cancellation, mastering fetch
can make your applications faster, more reliable, and easier to maintain.
Key Takeaways:
useEffect
and hooks.