The Fetch API is the standard, modern interface for requesting resources on the web—everything from JSON data to images, files, and more. It’s available as a global function in both window and web worker contexts, which makes it broadly useful whether you’re building a UI-driven app or doing background work off the main thread.
At the center of Fetch is a simple idea: use fetch() to make a request, then work with a Response object to decide how to handle the result.
The Core Mental Model
fetch() is promise-based
Calling fetch(url) returns a Promise that resolves to a Response object as soon as the server responds with headers—not necessarily when you’ve finished reading the body. From there, you choose how to consume the body:
- response.json() for JSON APIs
- response.text() for text
- response.blob() for binary data (like images/files)
- and other body-reading methods depending on your needs
This design is one of Fetch’s big strengths: you separate requesting from parsing, and you choose the most appropriate format for each situation.
Fetch uses Request and Response
Under the hood, Fetch is built around Request and Response objects (plus Headers). Most of the time you’ll just call fetch(), but knowing these objects exist helps when you start doing more advanced work (custom headers, different request modes, request reuse, etc.).
The “Gotcha” Everyone Should Know
Fetch is intentionally low-level, and that includes error handling.
Important behavior: fetch() typically does not reject the promise just because the server returned an HTTP error like 404 or 500. Instead, the promise resolves normally, and you’re expected to check the response status yourself.
In practice, that means good Fetch usage often follows this pattern:
- await fetch(...)
- Check response.ok (or inspect response.status)
- Parse the body (json(), text(), etc.)
- Handle exceptions (network failures, parsing failures, thrown errors)
This is a major difference from some higher-level HTTP libraries, and it’s why a manual response.ok check is so common in real-world code.
Making Requests: GET, POST, and Beyond
Fetch supports all standard HTTP methods. A simple GET is just fetch(url), while requests like POST/PUT/PATCH typically pass a second argument (often called an “init” object) where you configure:
- method
- headers
- body
For JSON APIs, you’ll usually set a Content-Type: application/json header and pass JSON.stringify(...) into the body.
Why Fetch Replaced XMLHttpRequest
The Fetch API is widely seen as a more powerful and flexible replacement for XMLHttpRequest because it’s:
- Promise-based, which fits modern async code (async/await)
- Built around standardized Request/Response primitives
- Designed to integrate cleanly with other web platform concepts (like headers, CORS, and service workers)
Fetch API Demo
Fetch API Overview
Check the Console for detailed logs, or click below to run scenarios.
Response Output:
Waiting for interaction...
:root {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--card-bg: #2d2d2d;
--primary: #4caf50;
--secondary: #2196f3;
--danger: #f44336;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
padding: 2rem;
line-height: 1.6;
}
.container {
max-width: 600px;
width: 100%;
}
h1 {
margin-bottom: 0.5rem;
}
p {
opacity: 0.8;
margin-top: 0;
}
.controls {
display: flex;
gap: 10px;
margin: 2rem 0;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
color: white;
transition: opacity 0.2s;
}
.btn:hover {
opacity: 0.9;
}
.btn:active {
transform: translateY(1px);
}
.primary {
background-color: var(--primary);
}
.danger {
background-color: var(--danger);
}
.secondary {
background-color: var(--secondary);
}
.output-area {
background-color: var(--card-bg);
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
pre {
background: #000;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 0.9rem;
color: #a5d6a7;
white-space: pre-wrap;
}
/**
* Spec Steppin' Series: Fetch API Overview
* Source: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
*/
const output = document.querySelector('#output');
const BASE_URL = 'https://jsonplaceholder.typicode.com';
// Helper to print JSON to the screen
const logToScreen = (data) => {
output.textContent = JSON.stringify(data, null, 2);
};
// 1. Basic GET Request
const fetchUser = async (userId) => {
console.log(`\n--- Fetching User ID: ${userId} ---`);
try {
const response = await fetch(`${BASE_URL}/users/${userId}`);
// CRITICAL: Check if request was successful (fetch doesn't reject on 404)
if (!response.ok) {
throw new Error(`HTTP Error! Status: ${response.status}`);
}
const userData = await response.json();
console.log('User Data Retrieved:', userData);
// Return data so we can log it to the screen
return userData;
} catch (error) {
console.error('Fetch operation failed:', error.message);
return { error: error.message };
}
};
// 2. POST Request
const createPost = async (title, body) => {
console.log(`\n--- Creating New Post ---`);
const url = `${BASE_URL}/posts`;
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
body: JSON.stringify({
title,
body,
userId: 1
})
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`Failed to create post. Status: ${response.status}`);
}
const result = await response.json();
console.log('Post Created Successfully:', result);
return result;
} catch (error) {
console.error('Error creating post:', error.message);
return { error: error.message };
}
};
// --- UI Event Listeners ---
const btnSuccess = document.querySelector('#btn-success');
if (btnSuccess) {
btnSuccess.addEventListener('click', async () => {
output.textContent = 'Fetching User ID 1...';
const data = await fetchUser(1);
logToScreen(data);
});
}
const btnError = document.querySelector('#btn-error');
if (btnError) {
btnError.addEventListener('click', async () => {
output.textContent = 'Fetching User ID 9999 (Expect 404)...';
const data = await fetchUser(9999);
logToScreen(data);
});
}
const btnPost = document.querySelector('#btn-post');
if (btnPost) {
btnPost.addEventListener('click', async () => {
output.textContent = 'Creating Post...';
const data = await createPost('Spec Steppin', 'Fetch API Demo Content');
logToScreen(data);
});
}
Wrap-Up
If you’re building anything that talks to an API—whether it’s a small demo page or a full production app—the Fetch API is the baseline skill to have. Learn the core flow (fetch → Response → parse the body), remember that HTTP errors don’t automatically throw, and you’ll be ready to scale from simple requests to more advanced patterns like custom headers, structured error handling, and background fetching.

