The Fetch API is how JavaScript talks to the outside world — loading data from a server, submitting forms, updating records and deleting them. This guide covers GET, POST, PUT, DELETE, headers, error handling and real-world patterns, all explained in plain simple English.
Shashank ShekharJanuary 2025 · JavaScript
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 SheetA 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 Responsefetch('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 functiongetPost(id){try{constresponse=awaitfetch(`https://jsonplaceholder.typicode.com/posts/${id}`);constdata=awaitresponse.json();console.log(data);returndata;}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
constresponse=awaitfetch('https://api.example.com/data');// Status informationconsole.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 otherwiseconsole.log(response.url);// the URL that was actually fetched (after redirects)// Reading the body — pick ONE of these, you can only read the body onceconstjson=awaitresponse.json();// parse body as JSON objectconsttext=awaitresponse.text();// get body as plain textconstblob=awaitresponse.blob();// get body as binary (for images, files)constbuffer=awaitresponse.arrayBuffer();// raw bytes// Read a specific response headerconsole.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 functionfetchUser(id){try{constresponse=awaitfetch(`/api/users/${id}`);// response.ok is true for status codes 200–299 onlyif(!response.ok){// Manually throw so the catch block handles itthrownewError(`HTTP error — status: ${response.status}`);}constuser=awaitresponse.json();returnuser;}catch(error){// Handles both network errors and HTTP errorsconsole.error('Could not fetch user:',error.message);returnnull;}}
⚠️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 functioncreatePost(title,body){try{constresponse=awaitfetch('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 objectbody:JSON.stringify({title,body,userId:1})});if(!response.ok)thrownewError(`Error: ${response.status}`);constnewPost=awaitresponse.json();console.log('Created post with ID:',newPost.id);returnnewPost;}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
constformEl=document.getElementById('loginForm');constformData=newFormData(formEl);// Or build FormData manuallyconstdata=newFormData();data.append('email','shashank@example.com');data.append('password','secret123');constresponse=awaitfetch('/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 dataasync functionreplacePost(id,postData){constresponse=awaitfetch(`/api/posts/${id}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(postData)});if(!response.ok)thrownewError(`Error: ${response.status}`);returnresponse.json();}// PATCH — update only the title, leave everything else as-isasync functionupdateTitle(id,newTitle){constresponse=awaitfetch(`/api/posts/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:newTitle})// only send what changed});if(!response.ok)thrownewError(`Error: ${response.status}`);returnresponse.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 functiondeletePost(id){try{constresponse=awaitfetch(`/api/posts/${id}`,{method:'DELETE'});if(!response.ok)thrownewError(`Error: ${response.status}`);// 204 No Content means success — no body to parseif(response.status===204){console.log(`Post ${id} deleted successfully.`);returntrue;}returnresponse.json();// some APIs return a body on DELETE}catch(error){console.error('Delete failed:',error.message);returnfalse;}}
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
consttoken=localStorage.getItem('authToken');async functiongetMyProfile(){constresponse=awaitfetch('/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 loginwindow.location.href='/login';return;}if(!response.ok)thrownewError(`Error: ${response.status}`);returnresponse.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 manuallyconstcontroller=newAbortController();async functionsearch(query){try{constresponse=awaitfetch(`/api/search?q=${query}`,{signal:controller.signal// link the request to this controller});returnresponse.json();}catch(error){if(error.name==='AbortError'){console.log('Request was cancelled.');// not a real error — handle quietly}else{throwerror;}}}// Cancel the request whenever you need tocontroller.abort();// Add a timeout — cancel if request takes more than 5 secondsasync functionfetchWithTimeout(url,timeoutMs=5000){constctrl=newAbortController();constid=setTimeout(()=>ctrl.abort(),timeoutMs);try{constres=awaitfetch(url,{signal:ctrl.signal});clearTimeout(id);// cancel the timeout if request finished in timereturnres;}catch(err){clearTimeout(id);throwerr;}}
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
constBASE_URL='https://api.yourapp.com';async functionapiFetch(endpoint,{method='GET',body,headers={}}={}){consttoken=localStorage.getItem('token');constconfig={method,headers:{'Content-Type':'application/json','Accept':'application/json',...(token&&{'Authorization':`Bearer ${token}`}),...headers},...(body&&{body:JSON.stringify(body)})};constresponse=awaitfetch(`${BASE_URL}${endpoint}`,config);if(response.status===401){localStorage.removeItem('token');window.location.href='/login';return;}if(!response.ok){consterrBody=awaitresponse.json().catch(()=>null);thrownewError(errBody?.message||`HTTP ${response.status}`);}if(response.status===204)returnnull;returnresponse.json();}// Usage — clean, no boilerplateconstposts=awaitapiFetch('/posts');constnewPost=awaitapiFetch('/posts',{method:'POST',body:{title:'Hello'}});awaitapiFetch(`/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
constAPI='https://jsonplaceholder.typicode.com';// ─ List posts ─────────────────────────────────────async functionloadPosts(){constres=awaitfetch(`${API}/posts?_limit=5`);constposts=awaitres.json();console.log('Posts loaded:',posts.map(p=>p.title));returnposts;}// ─ Create a post ──────────────────────────────────async functioncreatePost(title,body){constres=awaitfetch(`${API}/posts`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title,body,userId:1})});constpost=awaitres.json();console.log('Created:',post);returnpost;}// ─ Update a post title ────────────────────────────async functionupdatePostTitle(id,title){constres=awaitfetch(`${API}/posts/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({title})});constpost=awaitres.json();console.log('Updated title:',post.title);returnpost;}// ─ Delete a post ──────────────────────────────────async functiondeletePost(id){constres=awaitfetch(`${API}/posts/${id}`,{method:'DELETE'});console.log(`Deleted post ${id}: status ${res.status}`);}// ─ Run everything ─────────────────────────────────(async()=>{awaitloadPosts();awaitcreatePost('Hoopsiper Guide','A post from Fetch API article.');awaitupdatePostTitle(1,'Updated Title');awaitdeletePost(1);})();
Quick Reference
Method
Purpose
Has Body
Success Status
GET
Read data
No
200 OK
POST
Create new resource
Yes
201 Created
PUT
Replace whole resource
Yes
200 OK
PATCH
Update part of resource
Yes (partial)
200 OK
DELETE
Remove a resource
Usually no
204 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 SheetAll Fetch patterns — GET, POST, PUT, DELETE, headers and the reusable wrapper — in one downloadable file.