The Fetch API is the modern way to make HTTP requests in the browser. It replaced XMLHttpRequest with a cleaner, Promise-based interface. Firefox has full Fetch support including streaming, request cancellation, and custom headers. This guide covers practical patterns beyond the basics.
Basic Fetch
// Simple GET
const response = await fetch('/api/users');
const users = await response.json();
// Always check the status — fetch only rejects on network errors, not HTTP errors
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// POST with JSON body
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
},
body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }),
});
The Response Object
const response = await fetch('/api/data');
response.status // HTTP status code (200, 404, etc.)
response.ok // true if status is 200-299
response.statusText // "OK", "Not Found", etc.
response.headers // Headers object
// Body reading methods (each can only be called once)
await response.json() // Parse as JSON
await response.text() // Get as string
await response.blob() // Get as Blob (file data)
await response.arrayBuffer() // Raw bytes
await response.formData() // Form data
// Stream the body (advanced)
const reader = response.body.getReader();
Sending Different Body Types
// JSON
fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
// Form data (multipart, for file uploads)
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', 'My Document');
fetch('/api/upload', { method: 'POST', body: formData });
// Note: Don't set Content-Type header — browser sets it with boundary automatically
// URL-encoded form data
const params = new URLSearchParams({ key: 'value', other: '123' });
fetch('/api/form', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
});
// Raw binary
fetch('/api/binary', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: arrayBuffer,
});
Cancellation with AbortController
const controller = new AbortController();
// Cancel after 5 seconds
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/slow-endpoint', {
signal: controller.signal,
});
clearTimeout(timeoutId);
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request was cancelled');
} else {
throw err;
}
}
// Cancel previous search when user types
let searchController = null;
async function search(query) {
searchController?.abort(); // Cancel previous request
searchController = new AbortController();
try {
const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
});
return await resp.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
return null; // Cancelled, ignore
}
}
Streaming Responses
For large responses, stream them instead of loading everything into memory:
async function streamLargeFile(url, onProgress) {
const response = await fetch(url);
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength, 10);
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
onProgress(received / total);
}
const allChunks = new Uint8Array(received);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
return allChunks.buffer;
}
Server-Sent Events via Fetch
Stream data from the server in real-time:
async function streamAI(prompt, onToken) {
const response = await fetch('/api/ai/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
// Parse SSE format: "data: {...}\n\n"
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.token) onToken(data.token);
}
}
}
}
CORS and Fetch Modes
// Default: 'cors' — follows CORS rules, sends credentials if same-origin
fetch('https://api.example.com/data');
// 'no-cors': can't read response, but useful for firing analytics
fetch('https://analytics.example.com/track', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(event),
});
// 'same-origin': fail if cross-origin
fetch('/api/data', { mode: 'same-origin' });
// Sending cookies cross-origin
fetch('https://api.example.com/user', {
credentials: 'include', // Always send cookies
});
// 'same-origin' credentials (default): send cookies only if same origin
Retry Logic
async function fetchWithRetry(url, options, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
// Rate limited: wait and retry
const retryAfter = parseInt(response.headers.get('retry-after') || '1', 10);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (response.status >= 500 && attempt < maxRetries) {
// Server error: retry with backoff
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
return response;
} catch (err) {
if (err.name === 'AbortError') throw err; // Don't retry cancellations
lastError = err;
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
}
throw lastError;
}
Debugging Fetch in Firefox DevTools
Firefox's Network panel shows every fetch call:
- Open DevTools (F12) → Network tab
- Click any request to see:
- Request headers and body
- Response headers and body
- Timing: DNS lookup, connection, SSL handshake, TTFB, content download
- Right-click → Copy as Fetch: copies the request as a JavaScript
fetch()call you can run in the Console - Right-click → Resend to replay the request (with modifications)
The "XHR" filter shows only fetch/XMLHttpRequest calls, filtering out images/CSS/JS.
Top comments (0)