The JavaScript Fetch API lets your webpage request data from a server or send data to an API.
A product page can load products from an API. A search box can request matching results. A contact form can send form data without a full-page reload. A dashboard can request user details, orders, or reports after the page has loaded.
fetch() works with Promises, so everything you learned about JavaScript Promises and async/await now becomes useful in real API work.
Course Home: JavaScript Tutorial for Beginners
Previous Lesson: JavaScript Async/Await Explained
Next Lesson: JavaScript localStorage Explained
Quick Answer
A basic Fetch API request looks like this:
const response = await fetch(
"https://api.example.com/products"
);
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
const products = await response.json();
console.log(products);
The main steps are:
- Call
fetch(). - Wait for the
Response. - Check
response.ok. - Read the response body.
- Use the returned data.
- Handle errors.
A POST request can send JSON:
const response = await fetch(
"https://api.example.com/products",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
name: "Keyboard",
price: 1500
})
}
);
What Is the Fetch API?
The Fetch API is a browser API for making HTTP requests.
You can use it to communicate with:
- REST APIs
- Backend applications
- JSON endpoints
- Search services
- Form-processing endpoints
- Authentication endpoints
- Product APIs
- User APIs
- Your own server
The main function is:
fetch()
It returns a Promise that eventually fulfills with a:
Response
object.
Why the Fetch API Matters
Modern websites often load or send information after the first HTML page has loaded.
For example:
- Load product results
- Load user profiles
- Submit a contact form
- Add an item to a cart
- Update account settings
- Search without reloading the page
- Load dashboard statistics
- Request weather data
- Request blog posts
- Delete a saved item
- Update a record
The Fetch API gives JavaScript a standard way to make these requests.
fetch() Returns a Promise
Calling:
fetch("/api/products");
does not immediately return your product data.
It returns a Promise.
Example:
const request =
fetch("/api/products");
console.log(request);
The variable contains a Promise representing the future response.
You can handle it with:
.then()
or:
await
For this lesson, most examples use async/await because it is easier to read.
Basic Fetch API Syntax
The simplest syntax is:
fetch(url);
Example:
fetch("/api/products");
You can also provide an options object:
fetch(url, options);
Example:
fetch("/api/products", {
method: "POST"
});
The options can describe:
- HTTP method
- Headers
- Request body
- Credentials
- Cache behavior
- Abort signal
- Other request settings
Beginners mainly need:
method
headers
body
at first.
Your First GET Request
A GET request asks the server for data.
fetch() uses GET by default.
Example:
async function loadProducts() {
const response =
await fetch("/api/products");
console.log(response);
}
loadProducts();
The response variable contains a Response object.
It does not yet contain parsed JavaScript product data.
You still need to read the response body.
What Is the Response Object?
A Fetch request fulfills with a Response object.
Useful properties include:
response.ok
response.status
response.statusText
response.headers
response.url
response.redirected
Useful body-reading methods include:
response.json()
response.text()
response.blob()
response.arrayBuffer()
response.formData()
For APIs, response.json() is one of the most common methods.
Check response.ok
A successful Fetch Promise does not always mean the HTTP request returned a successful status.
For example, a server can respond with:
404
or:
500
and fetch() can still fulfill with a Response.
Check:
response.ok
Example:
async function loadProducts() {
const response =
await fetch("/api/products");
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
console.log("Request successful");
}
response.ok is true for successful HTTP status codes in the 200–299 range.
Check response.status
You can inspect the HTTP status code:
console.log(response.status);
Possible examples include:
200
201
204
400
401
403
404
500
You may use the status when your interface needs different behavior for different responses.
Example:
if (response.status === 404) {
throw new Error(
"Product was not found"
);
}
response.statusText
You may also inspect:
response.statusText
However, do not build important application logic around status text.
HTTP status codes are more reliable for program logic.
Use your own user-facing error messages when possible.
Read JSON With response.json()
Suppose an API returns:
[
{
"id": 1,
"name": "Keyboard",
"price": 1500
},
{
"id": 2,
"name": "Mouse",
"price": 700
}
]
Use:
const products =
await response.json();
Now products is normal JavaScript data.
Example:
console.log(products[0].name);
Output:
Keyboard
You learned JSON structure in JSON in JavaScript.
response.json() Returns a Promise
This is important.
response.json() does not return the parsed value immediately.
It returns another Promise.
So this:
const products =
response.json();
gives you a Promise.
Use:
const products =
await response.json();
inside an async function.
Complete GET Request Example
async function loadProducts() {
try {
const response =
await fetch("/api/products");
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
const products =
await response.json();
console.log(products);
} catch (error) {
console.error(
"Products could not be loaded",
error
);
}
}
loadProducts();
This is a strong basic pattern.
Step-by-Step GET Flow
The code:
const response =
await fetch("/api/products");
waits for the HTTP response.
Then:
if (!response.ok)
checks whether the HTTP status is successful.
Next:
const products =
await response.json();
reads and parses the JSON body.
Finally:
console.log(products);
uses the JavaScript data.
Real Website Example: Render Products
HTML:
<p id="status">Waiting...</p>
<div id="products"></div>
JavaScript:
const status =
document.querySelector("#status");
const productsElement =
document.querySelector("#products");
async function loadProducts() {
status.textContent = "Loading...";
try {
const response =
await fetch("/api/products");
if (!response.ok) {
throw new Error(
"Products could not be loaded"
);
}
const products =
await response.json();
productsElement.replaceChildren();
for (const product of products) {
const card =
document.createElement(
"article"
);
const title =
document.createElement(
"h2"
);
const price =
document.createElement(
"p"
);
title.textContent =
product.name;
price.textContent =
`₹${product.price}`;
card.append(
title,
price
);
productsElement.append(
card
);
}
status.textContent =
`${products.length} products loaded`;
} catch (error) {
status.textContent =
error.message;
}
}
loadProducts();
This combines Fetch with JavaScript DOM manipulation.
Fetch With .then()
You can also use Promise chaining.
Example:
fetch("/api/products")
.then((response) => {
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
return response.json();
})
.then((products) => {
console.log(products);
})
.catch((error) => {
console.error(error);
});
This is still valid modern JavaScript.
Use the style that keeps your code easiest to understand.
async/await vs .then()
Both work with Promises.
Promise chain
fetch("/api/products")
.then((response) => response.json())
.then((products) => {
console.log(products);
});
Async/await
async function loadProducts() {
const response =
await fetch("/api/products");
const products =
await response.json();
console.log(products);
}
For several sequential steps, async/await is often easier for beginners to follow.
GET Request With Query Parameters
APIs often use query parameters.
Example URL:
/api/products?category=laptop&page=2
You can write:
const response =
await fetch(
"/api/products?category=laptop&page=2"
);
For dynamic values, use URLSearchParams.
Build Query Parameters With URLSearchParams
Example:
const params =
new URLSearchParams({
category: "laptop",
page: "2"
});
const response =
await fetch(
`/api/products?${params}`
);
This helps encode query values correctly.
Real Website Example: Product Search
Suppose:
const searchTerm =
"wireless keyboard";
Build the request:
const params =
new URLSearchParams({
q: searchTerm
});
const response =
await fetch(
`/api/search?${params}`
);
The search value is encoded for the URL.
Do not manually join untrusted query values into URLs when URLSearchParams can handle the encoding.
What Is a POST Request?
A POST request commonly sends new data to a server.
Examples include:
- Create an account
- Submit a contact message
- Create an order
- Add a product
- Save a comment
- Submit application data
With Fetch:
fetch(url, {
method: "POST"
});
Send JSON With POST
Suppose:
const product = {
name: "Keyboard",
price: 1500
};
Send it:
const response =
await fetch(
"/api/products",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(product)
}
);
The JavaScript object is converted into JSON text before sending.
Why JSON.stringify() Is Needed
This is a JavaScript object:
const product = {
name: "Keyboard",
price: 1500
};
The JSON request body should be JSON text.
Use:
JSON.stringify(product)
You learned this conversion in the JSON tutorial.
Why Set Content-Type?
This header:
"Content-Type": "application/json"
tells the server that the request body contains JSON.
Example:
headers: {
"Content-Type":
"application/json"
}
The server must also be designed to accept JSON at that endpoint.
Do not assume every API expects the same body format.
Complete POST JSON Example
async function createProduct() {
const product = {
name: "Keyboard",
price: 1500
};
try {
const response =
await fetch(
"/api/products",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(
product
)
}
);
if (!response.ok) {
throw new Error(
`Request failed: ${response.status}`
);
}
const createdProduct =
await response.json();
console.log(createdProduct);
} catch (error) {
console.error(error);
}
}
This is a common create-record pattern.
HTTP Methods Used With Fetch
Common HTTP methods include:
| Method | Common purpose |
|---|---|
GET | Read data |
POST | Create or submit data |
PUT | Replace a resource |
PATCH | Update part of a resource |
DELETE | Remove a resource |
The exact meaning depends on the API design.
Fetch does not decide what an endpoint does. The server does.
PUT Request Example
const response =
await fetch(
"/api/products/101",
{
method: "PUT",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify({
name: "Keyboard",
price: 1400
})
}
);
An API may use PUT to replace the resource data.
Check that API’s documentation before choosing the method.
PATCH Request Example
A PATCH request often updates only part of a resource.
Example:
const response =
await fetch(
"/api/products/101",
{
method: "PATCH",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify({
price: 1400
})
}
);
Again, the server decides whether PATCH is supported.
DELETE Request Example
const response =
await fetch(
"/api/products/101",
{
method: "DELETE"
}
);
if (!response.ok) {
throw new Error(
"Product could not be deleted"
);
}
Some DELETE endpoints return JSON.
Others return no body.
Always follow the API contract.
Handle a 204 No Content Response
A successful API may return:
204 No Content
There is no response body to parse.
Do not blindly call:
await response.json();
after every request.
Example:
if (response.status === 204) {
console.log("Deleted");
return;
}
Then only parse JSON when the endpoint actually returns JSON.
Read Text With response.text()
Not every response contains JSON.
For plain text:
const response =
await fetch("/message.txt");
const text =
await response.text();
console.log(text);
Use the body-reading method that matches the actual response.
Read a Blob
A Blob is useful for binary-like browser data such as images or files.
Example:
const response =
await fetch("/images/photo.jpg");
const blob =
await response.blob();
console.log(blob);
Later, you can create an object URL:
const imageUrl =
URL.createObjectURL(blob);
Blob handling is more advanced, but useful to recognize.
Response Body Can Normally Be Read Once
A response body is a stream.
After you consume it with:
response.json()
you normally cannot call another body-reading method on the same consumed body.
For example, do not expect this to work normally:
const data =
await response.json();
const text =
await response.text();
Read the body in the format you need.
If advanced code genuinely needs more than one read, the Response can be cloned before consumption.
response.clone()
Example:
const response =
await fetch("/api/data");
const copy =
response.clone();
const json =
await response.json();
const text =
await copy.text();
This is not needed for normal beginner API work.
It is useful to know why a response body cannot simply be consumed repeatedly.
Send FormData With Fetch
You can send a browser FormData object.
HTML:
<form id="profileForm">
<input
name="name"
value="Riya"
>
<input
name="city"
value="Delhi"
>
</form>
JavaScript:
const form =
document.querySelector(
"#profileForm"
);
const formData =
new FormData(form);
const response =
await fetch(
"/api/profile",
{
method: "POST",
body: formData
}
);
When using FormData, do not manually set the multipart Content-Type header in the normal browser flow.
The browser adds the correct boundary information.
JSON vs FormData
Use JSON when the API expects JSON:
body:
JSON.stringify(data)
with:
"Content-Type":
"application/json"
Use FormData when the endpoint expects form-style multipart data or when uploading files.
Always follow the server’s expected request format.
Real Website Example: Submit a Contact Form
HTML:
<form id="contactForm">
<label for="name">Name</label>
<input
id="name"
name="name"
type="text"
required
>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
required
>
<label for="message">Message</label>
<textarea
id="message"
name="message"
required
></textarea>
<button
id="submitButton"
type="submit"
>
Send
</button>
</form>
<p id="status"></p>
JavaScript:
const contactForm =
document.querySelector(
"#contactForm"
);
const submitButton =
document.querySelector(
"#submitButton"
);
const status =
document.querySelector("#status");
contactForm.addEventListener(
"submit",
async (event) => {
event.preventDefault();
if (!contactForm.checkValidity()) {
contactForm.reportValidity();
return;
}
const formData =
new FormData(contactForm);
const data =
Object.fromEntries(
formData.entries()
);
submitButton.disabled = true;
status.textContent = "Sending...";
try {
const response =
await fetch(
"/api/contact",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(
data
)
}
);
if (!response.ok) {
throw new Error(
"Message could not be sent"
);
}
status.textContent =
"Message sent successfully";
contactForm.reset();
} catch (error) {
status.textContent =
error.message;
} finally {
submitButton.disabled =
false;
}
}
);
This combines form validation, events, JSON, async/await, and Fetch.
Client Validation Still Needs Server Validation
The previous example checks:
contactForm.checkValidity()
in the browser.
That improves the user experience.
But the server must validate the submitted data again.
Users can bypass or modify client-side JavaScript.
Never use frontend checks as the only protection for important data.
Review JavaScript Form Validation for the difference between client-side and server-side validation.
Request Headers
Headers provide metadata about the HTTP request.
Example:
headers: {
"Content-Type":
"application/json"
}
You may also see API-specific headers such as:
headers: {
"Accept":
"application/json"
}
or authentication-related headers.
Only send headers that the API expects.
Accept Header
The Accept header tells the server what response format the client prefers.
Example:
headers: {
"Accept":
"application/json"
}
It does not force the server to return JSON.
The server still decides what it supports.
Authorization Header
Some APIs use an authorization header.
Example shape:
headers: {
"Authorization":
`Bearer ${token}`
}
Do not hard-code private API secrets into public browser JavaScript.
Any secret delivered to frontend JavaScript can be inspected by users.
Browser applications should use authentication designs intended for public clients.
Do Not Put Server Secrets in Frontend Code
Avoid:
const secretApiKey =
"PRIVATE_SERVER_SECRET";
inside JavaScript sent to the browser.
Users can inspect:
- JavaScript files
- Network requests
- Browser storage
- Runtime values
If an API key must remain secret, request that API through a secure backend you control.
Fetch Error Handling
There are two important categories of failure.
Network-Level Failure
Examples:
- Network unavailable
- DNS problem
- Request blocked before a response
- Connection failure
- Request aborted
These can cause the Fetch Promise to reject.
HTTP Error Response
Examples:
400
401
403
404
500
These usually still produce a fulfilled Fetch Promise containing a Response.
Check:
response.ok
for HTTP success.
Strong Basic Error Pattern
async function requestData() {
try {
const response =
await fetch("/api/data");
if (!response.ok) {
throw new Error(
`HTTP ${response.status}`
);
}
const data =
await response.json();
return data;
} catch (error) {
console.error(error);
throw error;
}
}
The function logs the error and lets the caller handle it too.
Show Useful User Errors
Avoid displaying raw technical errors to normal users.
Instead of:
TypeError: Failed to fetch
your interface may show:
We could not load the products. Please try again.
Keep technical details in developer logs where appropriate.
User-facing messages should explain what the user can do next.
Real Website Example: Retry Button
HTML:
<p id="status"></p>
<button id="retryButton">
Try Again
</button>
JavaScript:
const status =
document.querySelector("#status");
const retryButton =
document.querySelector(
"#retryButton"
);
async function loadProducts() {
status.textContent = "Loading...";
retryButton.hidden = true;
try {
const response =
await fetch("/api/products");
if (!response.ok) {
throw new Error(
"Products could not be loaded"
);
}
const products =
await response.json();
status.textContent =
`${products.length} products loaded`;
} catch (error) {
status.textContent =
error.message;
retryButton.hidden = false;
}
}
retryButton.addEventListener(
"click",
loadProducts
);
loadProducts();
A retry option is often more useful than only showing an error.
Loading, Success, Empty and Error States
A robust API interface should think about at least four states.
Loading
Loading products...
Success
12 products loaded
Empty
No products found
Error
Products could not be loaded
Do not assume every successful API response contains useful results.
Handle an Empty Array
Example:
const products =
await response.json();
if (
Array.isArray(products) &&
products.length === 0
) {
status.textContent =
"No products found";
return;
}
A successful empty response is different from a failed request.
Validate API Data
A successful JSON response can still contain unexpected data.
Suppose your code expects:
{
name: "Keyboard",
price: 1500
}
but receives:
{
name: null,
price: "free"
}
The JSON is valid.
The data may still be unusable for your application.
Check important values before relying on them.
Example:
if (
typeof product.name !== "string" ||
typeof product.price !== "number"
) {
throw new Error(
"Unexpected product data"
);
}
Fetch and CORS
You may see a browser error mentioning:
CORS
CORS stands for Cross-Origin Resource Sharing.
Browser security rules control whether JavaScript from one origin can read responses from another origin.
For example, your site may request:
https://api.example.com
from:
https://www.yoursite.com
The API server must allow the cross-origin request when browser CORS rules require permission.
You Cannot Fix Server CORS With a Random Fetch Option
A common beginner mistake is trying to solve CORS only by changing JavaScript.
CORS permission is primarily controlled by the server receiving the request.
If an API does not allow your origin, browser JavaScript cannot simply override that security policy.
Use an API that supports your frontend origin or send the request through an appropriate backend you control.
Do Not Use no-cors as a Normal CORS Fix
You may see:
fetch(url, {
mode: "no-cors"
});
This does not give normal access to a blocked cross-origin API response.
It can produce an opaque response that JavaScript cannot inspect normally.
Do not use no-cors as a shortcut for APIs that should return readable data to your application.
Same-Origin and Cross-Origin
An origin includes:
- Protocol
- Host
- Port
These can be different origins:
https://example.com
https://api.example.com
because the host differs.
Even subdomains can be separate origins.
CORS rules can apply when browser JavaScript requests another origin.
Cookies and Credentials
Fetch has a:
credentials
option.
A request to the same origin normally includes credentials according to the default Fetch behavior.
For some cross-origin authentication setups, an API may require:
credentials: "include"
Example:
const response =
await fetch(
"https://api.example.com/account",
{
credentials: "include"
}
);
Cross-origin credentialed requests also require correct server-side CORS configuration.
Do not add credentials: "include" automatically to every request.
Abort a Fetch Request
You can cancel a request with AbortController.
Example:
const controller =
new AbortController();
const responsePromise =
fetch(
"/api/products",
{
signal:
controller.signal
}
);
controller.abort();
The request is aborted.
This can be useful when:
- The user leaves a page section
- A newer search replaces an older search
- You implement a timeout
- A component is removed
Handle Abort Errors
Example:
async function loadProducts() {
const controller =
new AbortController();
try {
const response =
await fetch(
"/api/products",
{
signal:
controller.signal
}
);
return await response.json();
} catch (error) {
if (
error.name === "AbortError"
) {
console.log(
"Request was cancelled"
);
return;
}
throw error;
}
}
Only treat an abort as a normal cancellation when that is your intended behavior.
Fetch Timeout With AbortController
Fetch does not traditionally use a simple timeout option in the same way some older libraries do.
You can build one with AbortController.
Example:
async function fetchWithTimeout(
url,
milliseconds
) {
const controller =
new AbortController();
const timeoutId =
setTimeout(() => {
controller.abort();
}, milliseconds);
try {
const response =
await fetch(
url,
{
signal:
controller.signal
}
);
return response;
} finally {
clearTimeout(timeoutId);
}
}
Use timeouts only when they make sense for your application and network expectations.
Real Website Example: Live Search
Suppose users type quickly into a search box.
You may want a newer request to cancel an older one.
HTML:
<input
id="search"
type="search"
placeholder="Search products"
>
<p id="status"></p>
JavaScript:
const search =
document.querySelector("#search");
const status =
document.querySelector("#status");
let controller;
search.addEventListener(
"input",
async () => {
const query =
search.value.trim();
if (query === "") {
status.textContent = "";
return;
}
controller?.abort();
controller =
new AbortController();
const params =
new URLSearchParams({
q: query
});
try {
const response =
await fetch(
`/api/search?${params}`,
{
signal:
controller.signal
}
);
if (!response.ok) {
throw new Error(
"Search failed"
);
}
const results =
await response.json();
status.textContent =
`${results.length} results`;
} catch (error) {
if (
error.name ===
"AbortError"
) {
return;
}
status.textContent =
"Search failed";
}
}
);
This prevents an older request from continuing to control the latest search state.
For production search, you may also debounce input requests.
Fetch Several Resources With Promise.all()
Suppose a page needs users and products.
If the requests are independent:
async function loadPage() {
const [
usersResponse,
productsResponse
] = await Promise.all([
fetch("/api/users"),
fetch("/api/products")
]);
if (
!usersResponse.ok ||
!productsResponse.ok
) {
throw new Error(
"Page data could not be loaded"
);
}
const [
users,
products
] = await Promise.all([
usersResponse.json(),
productsResponse.json()
]);
console.log(users);
console.log(products);
}
Independent requests can begin together.
Review JavaScript Promises for Promise.all().
Do Not Run Independent Requests Sequentially Without Need
This waits for the first request before starting the second:
const userResponse =
await fetch("/api/user");
const productsResponse =
await fetch("/api/products");
If neither depends on the other, starting them together can reduce waiting:
const [
userResponse,
productsResponse
] = await Promise.all([
fetch("/api/user"),
fetch("/api/products")
]);
Sequential Fetch When Requests Depend on Each Other
Sometimes sequential requests are correct.
Example:
const userResponse =
await fetch("/api/user");
const user =
await userResponse.json();
const ordersResponse =
await fetch(
`/api/users/${user.id}/orders`
);
The second URL needs:
user.id
so it cannot start correctly before the user data is available.
Fetch With Authentication Tokens
Some APIs accept bearer tokens.
Example shape:
const response =
await fetch(
"/api/account",
{
headers: {
"Authorization":
`Bearer ${token}`
}
}
);
How tokens should be stored and sent depends on the application’s security design.
Do not treat localStorage or frontend variables as automatically safe places for sensitive authentication data.
Authentication security deserves its own dedicated architecture.
Reading Response Headers
You can inspect headers:
const contentType =
response.headers.get(
"content-type"
);
console.log(contentType);
This can help determine the returned format.
Not every response header is exposed to cross-origin JavaScript unless the server allows it.
Check Content-Type Before Parsing JSON
For defensive code:
const contentType =
response.headers.get(
"content-type"
);
if (
contentType?.includes(
"application/json"
)
) {
const data =
await response.json();
console.log(data);
}
This can prevent treating an unexpected HTML or text error response as JSON.
The exact media type may include additional parameters.
API Error Body
Some APIs return useful JSON error data.
Example:
{
"message": "Email already exists"
}
You may want to read that body even when response.ok is false.
Example:
async function createUser(data) {
const response =
await fetch(
"/api/users",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(data)
}
);
const result =
await response.json();
if (!response.ok) {
throw new Error(
result.message ||
"Request failed"
);
}
return result;
}
Only do this when the API is documented to return JSON for both success and error responses.
Avoid Assuming Every Error Body Is JSON
A server may return:
- JSON
- Text
- HTML
- No body
If you do not control the API, inspect its documentation before assuming:
response.json()
will always work.
Real Website Example: Create Account
async function createAccount(
accountData
) {
const response =
await fetch(
"/api/accounts",
{
method: "POST",
headers: {
"Content-Type":
"application/json",
"Accept":
"application/json"
},
body:
JSON.stringify(
accountData
)
}
);
const result =
await response.json();
if (!response.ok) {
throw new Error(
result.message ||
"Account could not be created"
);
}
return result;
}
Call:
async function signup() {
try {
const account =
await createAccount({
name: "Riya",
email:
"riya@example.com"
});
console.log(account);
} catch (error) {
console.error(
error.message
);
}
}
Fetch and DOM Safety
API data should be treated as external data.
Avoid:
resultElement.innerHTML =
product.description;
when the API content is not trusted HTML.
Prefer:
resultElement.textContent =
product.description;
for normal text.
Fetch only retrieves data. It does not make that data safe to insert as HTML.
Real Website Example: Safe Product Rendering
const card =
document.createElement("article");
const title =
document.createElement("h2");
const description =
document.createElement("p");
title.textContent =
product.name;
description.textContent =
product.description;
card.append(
title,
description
);
Using DOM methods and textContent is a safer default for untrusted text.
Fetch and Caching
Browsers and servers can cache HTTP responses according to normal web caching rules.
Fetch also has a:
cache
option.
Example:
fetch("/api/data", {
cache: "no-store"
});
Do not disable caching automatically.
Caching can improve performance.
Follow your API’s freshness requirements instead of forcing every request to bypass cache.
Fetch and Request Objects
You may also create a:
Request
object.
Example:
const request =
new Request(
"/api/products",
{
method: "GET"
}
);
const response =
await fetch(request);
Most beginner code can pass the URL and options directly to fetch().
Request objects become useful when request configuration needs to be reused or passed around.
Common Beginner Mistakes
Forgetting That fetch() Returns a Promise
Wrong:
const data =
fetch("/api/products");
console.log(data[0]);
data is a Promise, not the product array.
Use await or .then().
Forgetting That response.json() Returns a Promise
Wrong:
const data =
response.json();
console.log(data.name);
Use:
const data =
await response.json();
Assuming fetch() Rejects on 404 or 500
It normally does not.
Check:
response.ok
Forgetting JSON.stringify() for a JSON Body
Wrong:
body: {
name: "Keyboard"
}
when the API expects JSON.
Use:
body:
JSON.stringify({
name: "Keyboard"
})
Forgetting Content-Type for JSON
When the server expects JSON, include:
"Content-Type":
"application/json"
Manually Setting Content-Type for FormData
Do not normally set multipart boundary headers yourself when passing a FormData object directly to Fetch.
Let the browser create the correct content type.
Parsing a 204 Response as JSON
A 204 No Content response has no body.
Do not call response.json() blindly.
Reading the Same Response Body Twice
After the body is consumed, it cannot normally be consumed again.
Showing Raw API Errors to Users
Technical error messages may be confusing or expose unnecessary details.
Log technical information separately and show a useful user message.
Trusting API Data Without Checking It
Valid JSON can still contain wrong types or missing properties.
Validate important values.
Putting Private API Keys in Browser JavaScript
Frontend code is visible to users.
Keep secrets on a secure server.
Trying to Fix CORS With no-cors
mode: "no-cors" does not give normal readable access to a CORS-blocked API.
The server must support the browser request.
Forgetting Loading and Empty States
A successful request can still take time or return no records.
Design for:
- Loading
- Success
- Empty
- Error
Sending a Request on Every Keystroke Without Control
Live search can create many requests.
Use cancellation and, when appropriate, debouncing.
Running Independent Fetch Requests Sequentially
Use Promise.all() when the requests are independent and all are required.
Using Promise.all() for Dependent Requests
When request two needs data from request one, use sequential await.
Forgetting finally for UI Cleanup
If you disable a button before a request, restore it after failure when appropriate.
Example:
try {
// request
} finally {
button.disabled = false;
}
Best Practices for the JavaScript Fetch API
Use async/await when it makes request flow easier to read.
Check response.ok before treating an HTTP response as successful.
Use the body reader that matches the response format.
Use JSON.stringify() when an API expects a JSON request body.
Set Content-Type: application/json only when sending JSON.
Let the browser set multipart boundaries for FormData.
Handle network errors with try...catch.
Handle HTTP errors explicitly.
Show useful loading, empty, success, and error states.
Validate important data received from APIs.
Use URLSearchParams for dynamic query parameters.
Use AbortController when stale requests should be cancelled.
Use Promise.all() for independent requests that can begin together.
Never place private server secrets in browser code.
Treat API text as untrusted when inserting it into the DOM.
Keep request logic in focused functions so it can be reused and tested.
Beginner Exercise
Use this API-style URL:
https://jsonplaceholder.typicode.com/posts/1
Create:
async function loadPost() {
// your code
}
Complete these steps:
- Call
fetch(). - Check
response.ok. - Read the JSON.
- Print the post title.
- Print the post body.
- Handle errors with
try...catch.
Then call:
loadPost();
Challenge Exercise
Use:
https://jsonplaceholder.typicode.com/posts
Create a POST request with this data:
const post = {
title: "Learning Fetch API",
body: "My first POST request",
userId: 1
};
Requirements:
- Use method
POST. - Set
Content-Typetoapplication/json. - Convert the object with
JSON.stringify(). - Check
response.ok. - Read the JSON response.
- Print the returned object.
- Handle errors.
Extra Challenge
Create a small page with:
<button id="loadButton">
Load Posts
</button>
<p id="status"></p>
<div id="posts"></div>
When the button is clicked:
- Change the status to
Loading.... - Fetch posts.
- Show the first five post titles.
- Show
No posts foundif the returned array is empty. - Show an error message if the request fails.
- Disable the button during the request.
- Enable it again inside
finally.
Create each title with:
document.createElement()
and use:
textContent
rather than inserting API data with innerHTML.
Frequently Asked Questions
What is the Fetch API in JavaScript?
The Fetch API is a browser API for making HTTP requests and receiving responses.
The main function is:
fetch()
Does fetch() return a Promise?
Yes.
fetch() returns a Promise that fulfills with a Response object when a response is available, unless the request fails at the network level or is otherwise rejected.
How do I make a GET request with fetch?
GET is the default:
const response =
await fetch("/api/products");
How do I read JSON from a Fetch response?
Use:
const data =
await response.json();
Does response.json() return a Promise?
Yes.
It asynchronously reads the response body and parses it as JSON.
How do I check whether a fetch request succeeded?
Check:
response.ok
You can also inspect:
response.status
Does fetch() reject on a 404 response?
Not normally.
A 404 is still an HTTP response.
Check response.ok or response.status.
How do I send a POST request with fetch?
Example:
await fetch("/api/products", {
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(data)
});
Why do I need JSON.stringify() in a POST request?
When the server expects JSON text, JSON.stringify() converts your JavaScript object into JSON text for the request body.
What does Content-Type application/json mean?
It tells the server that the request body contains JSON.
What is the difference between response.json() and JSON.parse()?
response.json() reads and parses a Fetch Response body asynchronously.
JSON.parse() parses a JSON string you already have.
Can fetch send FormData?
Yes.
Pass the FormData object as the request body.
body: formData
Should I set Content-Type manually with FormData?
Normally, no.
The browser adds the multipart boundary when sending FormData.
How do I send query parameters with fetch?
You can build them with:
URLSearchParams
then add them to the URL.
How do I make a DELETE request?
Example:
await fetch(
"/api/products/101",
{
method: "DELETE"
}
);
Follow the API’s documented method and response format.
What is response.ok?
It is a boolean indicating whether the HTTP status is in the successful 200–299 range.
What is response.status?
It is the numeric HTTP status code, such as 200, 404, or 500.
What happens with a 204 response?
A 204 response has no content body.
Do not try to parse JSON unless the endpoint actually returns a body.
Can I read a Fetch response twice?
Not normally after the body has been consumed.
Use the correct body-reading method once, or clone the Response first when advanced code genuinely requires another read.
What is CORS?
CORS is a browser security mechanism controlling cross-origin access to HTTP responses.
The server must provide suitable permission for allowed cross-origin requests.
Can I fix CORS with JavaScript?
Not when the server refuses the browser’s origin.
The API server must be configured to allow the request, or your application needs an appropriate server-side architecture.
Does mode no-cors fix CORS?
No.
It does not provide normal readable access to a cross-origin response that is otherwise blocked by CORS.
Can I cancel a fetch request?
Yes.
Use:
AbortController
and pass its signal to fetch().
Can I set a timeout for fetch?
You can create timeout behavior with AbortController and a timer.
Can I send several fetch requests at the same time?
Yes.
For independent requests, use:
Promise.all()
when all results are required.
Is Fetch secure?
Fetch is only a request API.
Security depends on HTTPS, authentication, authorization, server validation, CORS configuration, safe data handling, and your overall application design.
Should I put API keys in JavaScript?
Never put private server secrets into JavaScript delivered to the browser.
Users can inspect frontend code and network requests.
How should I display API data in HTML?
For normal text from an API, prefer DOM methods and:
textContent
Avoid inserting untrusted API strings with innerHTML.
What should I learn after the Fetch API?
Learn JavaScript localStorage next. It lets your browser save small amounts of string data that remain available across page reloads.
Summary
The JavaScript Fetch API lets your webpage communicate with servers and APIs.
A basic GET request looks like:
const response =
await fetch("/api/products");
if (!response.ok) {
throw new Error(
`HTTP error: ${response.status}`
);
}
const products =
await response.json();
A JSON POST request looks like:
await fetch("/api/products", {
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body:
JSON.stringify(product)
});
You also learned how to:
- Make GET requests
- Read the
Responseobject - Check
response.ok - Inspect HTTP status codes
- Parse JSON
- Read text and Blob responses
- Send POST requests
- Send PUT and PATCH requests
- Make DELETE requests
- Handle 204 responses
- Send JSON bodies
- Send FormData
- Build query strings
- Use request headers
- Handle network errors
- Handle HTTP errors
- Work with CORS
- Avoid exposing private API secrets
- Cancel requests with
AbortController - Run independent requests with
Promise.all() - Build loading, success, empty, and error states
- Render API data safely into the DOM
Fetch brings together many JavaScript concepts you have already learned: Promises, async/await, JSON, objects, arrays, events, forms, and DOM manipulation.
Continue Learning JavaScript
Previous Lesson: JavaScript Async/Await Explained
Course Home: JavaScript Tutorial for Beginners
Next Lesson: JavaScript localStorage Explained
In the next lesson, you will learn how to save strings, objects, arrays, user preferences, and simple cart data in the browser with localStorage, JSON.stringify(), and JSON.parse().
