Almost every modern web application communicates with a server — loading a user's profile, posting a comment, fetching product data from a store. In JavaScript, this communication happens through the Fetch API.

Before Fetch, developers used XMLHttpRequest, which was clunky and hard to read. Fetch replaced it with a clean, Promise-based interface that works naturally with modern async/await syntax.

This guide covers everything you need to make real HTTP requests — from your very first GET call to building a reusable helper that handles authentication, errors and timeouts automatically.

Download This Article as a Cheat Sheet A clean text summary of all Fetch API patterns — GET, POST, PUT, DELETE, headers and error handling — for quick offline reference.
TXT  ·  1 page  ·  Free

What Is the Fetch API

The Fetch API is a built-in browser function that lets your JavaScript code send HTTP requests and receive responses. You use it to talk to APIs — both your own backend and third-party services.

Think of it like a waiter in a restaurant. You (the browser) tell the waiter (Fetch) what you want. The waiter goes to the kitchen (the server), picks up the food (data) and brings it back to you. While the waiter is gone, you can do other things — you do not have to sit frozen waiting. This is why Fetch is asynchronous.

Fetch works with Promises. A Promise is an object that represents a future result — something that has not arrived yet but will eventually. You attach .then() to run code when it succeeds, or use async/await to write it like normal code.


Your First Fetch Request

The simplest Fetch call takes a URL and returns a Promise that resolves to a Response object. Here is what that looks like in two styles — Promise chains and async/await.

Using .then() Chains

JavaScript — basic GET with .then()
// fetch() returns a Promise that resolves to a Response fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => response.json()) // parse the body as JSON .then(data => { console.log(data.title); console.log(data.body); }) .catch(error => { console.error('Request failed:', error); }); // .then() gives you the Response, then you call .json() to get the actual data // .json() is itself async — it returns another Promise // That is why you need two .then() calls

Using async/await — The Cleaner Style

Most developers prefer async/await because it reads like regular step-by-step code instead of a chain of callbacks. The await keyword pauses execution until the Promise resolves, without blocking anything else in the browser.

JavaScript — the same GET request using async/await
async function getPost(id) { try { const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); const data = await response.json(); console.log(data); return data; } catch (error) { console.error('Failed to load post:', error.message); } } getPost(1);
ℹ️ async functions always return a Promise. You cannot use await outside an async function. If you call an async function from the top level of a script, either put it in an async IIFE or use .then() on the returned Promise.

The Response Object — What Fetch Gives You Back

When a Fetch request completes, the first Promise resolves to a Response object. This object has information about the HTTP response — the status code, headers and the raw body. The body is a stream, so you have to call one of its parsing methods to get the actual data.

JavaScript — exploring the Response object
const response = await fetch('https://api.example.com/data'); // Status information console.log(response.status); // 200, 404, 500, etc. console.log(response.statusText); // "OK", "Not Found", etc. console.log(response.ok); // true if status is 200-299, false otherwise console.log(response.url); // the URL that was actually fetched (after redirects) // Reading the body — pick ONE of these, you can only read the body once const json = await response.json(); // parse body as JSON object const text = await response.text(); // get body as plain text const blob = await response.blob(); // get body as binary (for images, files) const buffer = await response.arrayBuffer(); // raw bytes // Read a specific response header console.log(response.headers.get('Content-Type')); // e.g. "application/json; charset=utf-8"

Error Handling — The Most Important Part

This is where a lot of beginners get tripped up. Fetch has a behaviour that surprises many people: it does not throw an error for HTTP error codes like 404 or 500. It only throws an error for actual network failures (like being offline or the server being unreachable). For everything else, it considers the request a success and gives you a Response object.

This means you have to check the status code yourself.

Network Errors

JavaScript — what kind of errors Fetch throws automatically
// These cause Fetch to throw (reject the Promise): // - No internet connection // - DNS lookup failed (domain does not exist) // - Server refused connection // - Request was aborted // - CORS blocked by the server // These do NOT cause Fetch to throw — you get a Response object back: // - 400 Bad Request // - 401 Unauthorized // - 403 Forbidden // - 404 Not Found // - 500 Server Error // This is why you must ALWAYS check response.ok

Handling HTTP Errors Properly

JavaScript — the correct way to handle all error types
async function fetchUser(id) { try { const response = await fetch(`/api/users/${id}`); // response.ok is true for status codes 200–299 only if (!response.ok) { // Manually throw so the catch block handles it throw new Error(`HTTP error — status: ${response.status}`); } const user = await response.json(); return user; } catch (error) { // Handles both network errors and HTTP errors console.error('Could not fetch user:', error.message); return null; } }
⚠️ Always check response.ok. A 404 or 500 response will not trigger your catch block by itself. You must check if (!response.ok) and throw manually. Skipping this check is one of the most common bugs in JavaScript API code.

POST — Sending Data to the Server

A GET request only reads data. A POST request sends data to the server — to create a new record, submit a form or trigger an action. You pass a second argument to fetch() with the method, headers and body.

Sending JSON Data

JavaScript — POST request sending JSON to an API
async function createPost(title, body) { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { // Tell the server you are sending JSON 'Content-Type': 'application/json' }, // Body must be a string — JSON.stringify converts the object body: JSON.stringify({ title, body, userId: 1 }) }); if (!response.ok) throw new Error(`Error: ${response.status}`); const newPost = await response.json(); console.log('Created post with ID:', newPost.id); return newPost; } catch (error) { console.error('Failed to create post:', error.message); } } createPost('My First Post', 'This is the body of the post.');

Sending HTML Form Data

When submitting form data (like a login form), use FormData instead of JSON. You do not need to set the Content-Type header — the browser sets it automatically, including the required boundary string.

JavaScript — POST with FormData for file uploads and forms
const formEl = document.getElementById('loginForm'); const formData = new FormData(formEl); // Or build FormData manually const data = new FormData(); data.append('email', 'shashank@example.com'); data.append('password', 'secret123'); const response = await fetch('/api/auth/login', { method: 'POST', body: data // No Content-Type header needed — browser sets it automatically for FormData });

PUT and PATCH — Updating Records

PUT replaces the entire resource with the data you send. PATCH updates only the fields you include. Both follow the same pattern as POST — you just change the method string.

JavaScript — PUT to replace and PATCH to update partially
// PUT — replace the whole post with this data async function replacePost(id, postData) { const response = await fetch(`/api/posts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postData) }); if (!response.ok) throw new Error(`Error: ${response.status}`); return response.json(); } // PATCH — update only the title, leave everything else as-is async function updateTitle(id, newTitle) { const response = await fetch(`/api/posts/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTitle }) // only send what changed }); if (!response.ok) throw new Error(`Error: ${response.status}`); return response.json(); }

DELETE — Removing Records

DELETE requests remove a resource. They usually have no body. A successful DELETE typically returns 204 No Content, which means there is no JSON body to parse.

JavaScript — DELETE request
async function deletePost(id) { try { const response = await fetch(`/api/posts/${id}`, { method: 'DELETE' }); if (!response.ok) throw new Error(`Error: ${response.status}`); // 204 No Content means success — no body to parse if (response.status === 204) { console.log(`Post ${id} deleted successfully.`); return true; } return response.json(); // some APIs return a body on DELETE } catch (error) { console.error('Delete failed:', error.message); return false; } }

Headers — Sending Extra Information

Headers are key-value pairs that travel with every HTTP request and response. They carry metadata — what type of content you are sending, your authentication token, which language you prefer and much more.

Bearer Token Authentication

Most APIs require authentication. The standard way is to include a Bearer token in the Authorization header. You get the token when you log in and include it in every subsequent request.

JavaScript — sending custom headers and a Bearer token
const token = localStorage.getItem('authToken'); async function getMyProfile() { const response = await fetch('/api/profile', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Client-Version': '2.0' // custom header your API expects } }); if (response.status === 401) { // Token expired or invalid — redirect to login window.location.href = '/login'; return; } if (!response.ok) throw new Error(`Error: ${response.status}`); return response.json(); }

Cancelling Requests — AbortController

Sometimes you need to cancel a request that is already in flight — for example when a user navigates away from the page, types something new in a search box before the previous result arrives, or you want to add a timeout. AbortController lets you do this.

JavaScript — cancelling a fetch with AbortController
// Cancel a request manually const controller = new AbortController(); async function search(query) { try { const response = await fetch(`/api/search?q=${query}`, { signal: controller.signal // link the request to this controller }); return response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Request was cancelled.'); // not a real error — handle quietly } else { throw error; } } } // Cancel the request whenever you need to controller.abort(); // Add a timeout — cancel if request takes more than 5 seconds async function fetchWithTimeout(url, timeoutMs = 5000) { const ctrl = new AbortController(); const id = setTimeout(() => ctrl.abort(), timeoutMs); try { const res = await fetch(url, { signal: ctrl.signal }); clearTimeout(id); // cancel the timeout if request finished in time return res; } catch (err) { clearTimeout(id); throw err; } }

Reusable Fetch Helper

Writing method, headers, JSON.stringify and error checking in every function gets repetitive fast. The standard solution is a small wrapper function that handles all the boilerplate and lets you focus on what actually matters.

JavaScript — a production-ready fetch wrapper
const BASE_URL = 'https://api.yourapp.com'; async function apiFetch(endpoint, { method = 'GET', body, headers = {} } = {}) { const token = localStorage.getItem('token'); const config = { method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }), ...headers }, ...(body && { body: JSON.stringify(body) }) }; const response = await fetch(`${BASE_URL}${endpoint}`, config); if (response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; return; } if (!response.ok) { const errBody = await response.json().catch(() => null); throw new Error(errBody?.message || `HTTP ${response.status}`); } if (response.status === 204) return null; return response.json(); } // Usage — clean, no boilerplate const posts = await apiFetch('/posts'); const newPost = await apiFetch('/posts', { method: 'POST', body: { title: 'Hello' } }); await apiFetch(`/posts/${id}`, { method: 'DELETE' });

Real World Example — A Full Mini App

Here is a complete working example that uses the JSONPlaceholder test API to list posts, create a new one and delete one — just like a real app would:

JavaScript — a mini posts app using all CRUD operations
const API = 'https://jsonplaceholder.typicode.com'; // ─ List posts ───────────────────────────────────── async function loadPosts() { const res = await fetch(`${API}/posts?_limit=5`); const posts = await res.json(); console.log('Posts loaded:', posts.map(p => p.title)); return posts; } // ─ Create a post ────────────────────────────────── async function createPost(title, body) { const res = await fetch(`${API}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, body, userId: 1 }) }); const post = await res.json(); console.log('Created:', post); return post; } // ─ Update a post title ──────────────────────────── async function updatePostTitle(id, title) { const res = await fetch(`${API}/posts/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); const post = await res.json(); console.log('Updated title:', post.title); return post; } // ─ Delete a post ────────────────────────────────── async function deletePost(id) { const res = await fetch(`${API}/posts/${id}`, { method: 'DELETE' }); console.log(`Deleted post ${id}: status ${res.status}`); } // ─ Run everything ───────────────────────────────── (async () => { await loadPosts(); await createPost('Hoopsiper Guide', 'A post from Fetch API article.'); await updatePostTitle(1, 'Updated Title'); await deletePost(1); })();

Quick Reference

MethodPurposeHas BodySuccess Status
GETRead dataNo200 OK
POSTCreate new resourceYes201 Created
PUTReplace whole resourceYes200 OK
PATCHUpdate part of resourceYes (partial)200 OK
DELETERemove a resourceUsually no204 No Content

⚡ Key Takeaways
  • fetch(url) returns a Promise that resolves to a Response object — not the data itself. You always need a second step (.json()) to get the actual data.
  • Use async/await with try/catch for the cleanest, most readable fetch code. It reads like normal synchronous code even though it is asynchronous.
  • fetch() does not throw for HTTP errors. A 404 or 500 gives you a Response object with ok: false. Always check if (!response.ok) and throw manually.
  • For POST, PUT and PATCH: set method, add 'Content-Type': 'application/json' to headers and set body: JSON.stringify(yourData).
  • FormData is for HTML form submissions and file uploads. Do not set Content-Type manually when using FormData — the browser sets it automatically with the correct boundary.
  • Send the Bearer token in every authenticated request using the Authorization: Bearer TOKEN header. Watch for 401 responses and redirect to login when the token expires.
  • Use AbortController to cancel in-flight requests and to add timeouts. Always handle AbortError silently in the catch block since it is intentional, not a real error.
  • Build a reusable fetch wrapper that handles base URL, auth headers, JSON stringifying and error checking in one place. This removes boilerplate from every API call in your app.
  • DELETE requests that succeed usually return 204 No Content — do not try to call .json() on a 204 response or you will get an error.
📄
Save This Guide as a Cheat Sheet All Fetch patterns — GET, POST, PUT, DELETE, headers and the reusable wrapper — in one downloadable file.
TXT  ·  1 page  ·  Free