fetch() Basics
fetch(url) sends an HTTP GET request and returns a Promise that resolves to a Response object when the server responds.
// GET request - returns a Response Promise
fetch('https://api.example.com/users')
.then(response => response.json()) // parse body as JSON
.then(users => console.log(users))
.catch(err => console.error('Network error:', err));
// With async/await (cleaner)
async function getUsers() {
const response = await fetch('https://api.example.com/users');
const users = await response.json();
return users;
}
// Pass URL options (query params)
const params = new URLSearchParams({ page: 1, limit: 20, sort: 'name' });
const response = await fetch('/api/users?' + params);
// URL: /api/users?page=1&limit=20&sort=name
// Full options object (GET is the default method)
const res = await fetch('/api/data', {
method: 'GET',
headers: { 'Accept': 'application/json' },
cache: 'no-cache', // 'default', 'no-store', 'reload', 'force-cache'
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
credentials: 'include', // 'omit', 'same-origin', 'include' (send cookies)
});
The Response Object
const response = await fetch('/api/user/1');
// Status
console.log(response.status); // 200, 404, 500, etc.
console.log(response.statusText); // 'OK', 'Not Found', etc.
console.log(response.ok); // true for 2xx status codes
// Headers
console.log(response.headers.get('Content-Type')); // 'application/json'
console.log(response.headers.get('X-Request-Id'));
// Reading the body (can only be read ONCE - it is a stream)
const json = await response.json(); // parse as JSON
const text = await response.text(); // read as string
const blob = await response.blob(); // for images/files
const buffer = await response.arrayBuffer(); // raw binary
// Clone before reading (if you need to read the body twice)
const clone = response.clone();
const body1 = await response.json();
const body2 = await clone.json(); // read the clone
// response.bodyUsed - true after body has been read
console.log(response.bodyUsed); // true after await response.json()
// Redirect handling (response.redirected tells you if it was followed)
console.log(response.url); // final URL after redirects
console.log(response.redirected); // true if request was redirected
POST with JSON
// POST with JSON body
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Must set Content-Type so server knows how to parse the body
},
body: JSON.stringify(userData), // object -> JSON string
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message ?? 'Request failed');
}
return response.json(); // server usually returns the created resource
}
const newUser = await createUser({ name: 'Alice', email: 'alice@example.com' });
console.log('Created:', newUser.id);
// PUT - update an existing resource
async function updateUser(id, data) {
const res = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
}
// PATCH - partial update
async function patchUser(id, data) {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
}
// DELETE
async function deleteUser(id) {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
return res.ok;
}
Headers
// Plain object headers (simplest for one-off requests)
const response = await fetch('/api/data', {
headers: {
'Authorization': 'Bearer ' + getToken(),
'Content-Type': 'application/json',
'X-Client-ID': 'my-app-v2',
}
});
// Headers class (more control)
const headers = new Headers();
headers.append('Authorization', 'Bearer ' + token);
headers.append('Content-Type', 'application/json');
headers.set('X-Custom', 'value'); // set replaces existing
headers.delete('X-Unwanted');
console.log(headers.has('Authorization')); // true
console.log(headers.get('Authorization')); // 'Bearer ...'
// Iterate all headers
for (const [name, value] of headers) {
console.log(name, ':', value);
}
// Reusable fetch wrapper with default headers
function apiFetch(url, options = {}) {
const token = localStorage.getItem('auth_token');
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': token ? 'Bearer ' + token : undefined,
...options.headers, // allow caller to add/override headers
},
});
}
// Usage
const res = await apiFetch('/api/profile');
const res2 = await apiFetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'multipart/form-data' }, // overrides default
});
Error Handling
fetch() only rejects on network failure. HTTP error status codes (4xx, 5xx) do NOT automatically reject - you must check response.ok.
// Common pattern: check response.ok before reading body
async function fetchJSON(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
// Try to read error message from server
let message = `HTTP ${response.status}`;
try {
const err = await response.json();
message = err.message ?? message;
} catch {
// Body wasn't JSON - use status text
message = response.statusText;
}
throw new Error(message);
}
return response.json();
}
// Usage with try/catch
try {
const user = await fetchJSON('/api/users/99');
renderUser(user);
} catch (err) {
if (err.message.startsWith('HTTP 404')) {
showNotFound();
} else if (err.message.startsWith('HTTP 401')) {
redirectToLogin();
} else {
showGenericError(err.message);
}
}
// Error types to handle:
// 1. Network error (no internet, DNS fail) - fetch() rejects with TypeError
// 2. CORS error - fetch() rejects with TypeError (no access to the response)
// 3. HTTP 4xx/5xx - must check response.ok manually
// 4. Invalid JSON body - response.json() rejects with SyntaxError
This is the most common fetch gotcha. A 404 Not Found or 500 Server Error response does not reject the Promise - the network request technically "succeeded". Always check if (!response.ok) before reading the body data.
File Upload with FormData
const fileInput = document.querySelector('input[type="file"]');
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('userId', '42');
// Do NOT set Content-Type - browser sets it with correct multipart boundary
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error('Upload failed');
return response.json(); // { url: 'https://...' }
}
fileInput.addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
// Client-side validation
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
if (file.size > MAX_SIZE) {
alert('File too large. Maximum 5 MB.');
return;
}
const ALLOWED = ['image/jpeg', 'image/png', 'image/webp'];
if (!ALLOWED.includes(file.type)) {
alert('Only JPEG, PNG, and WebP images allowed.');
return;
}
try {
const result = await uploadFile(file);
document.querySelector('#preview').src = result.url;
} catch (err) {
showError(err.message);
}
});
Advanced Patterns
// Pattern 1: Timeout with AbortController
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
return res;
} catch (err) {
if (err.name === 'AbortError') throw new Error('Request timed out');
throw err;
} finally {
clearTimeout(timer);
}
}
// Pattern 2: Cancel a request (e.g., on component unmount or new search)
let searchController = null;
async function search(query) {
searchController?.abort(); // cancel previous request
searchController = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
});
return res.json();
} catch (err) {
if (err.name === 'AbortError') return; // user typed more - ignore
throw err;
}
}
// Pattern 3: Retry on failure
async function fetchWithRetry(url, options, retries = 3, delay = 1000) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const res = await fetch(url, options);
if (res.ok) return res;
if (res.status < 500) throw new Error('Client error: ' + res.status);
// 5xx - retry
} catch (err) {
if (attempt === retries - 1) throw err;
}
await new Promise(r => setTimeout(r, delay * (attempt + 1))); // backoff
}
}