Fetch API Get Certified
The Fetch API is the modern way to make HTTP
requests from the browser. It returns Promises, works cleanly
with async/await, and replaces the
older XMLHttpRequest for most everyday use cases.
Because this tutorial runs in a sandboxed editor, we use a
mockFetch helper that behaves like the real
fetch() so every example produces visible output.
Basic Fetch Request
Calling fetch(url) returns a Promise that resolves
to a Response object. You then call a body method
such as .json() to read the data.
What this code does: Performs a basic mock GET request and logs the parsed JSON response.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: true,
status: 200,
json: function () {
return Promise.resolve({ id: 1, name: "Alice" });
}
});
}, 80);
});
}
console.log("Fetching user...");
mockFetch("/api/user")
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log("User:", data);
});
console.log("Request started");
function mockFetch(url) — stands in for the real fetch so the example runs offline.return new Promise(function (resolve) {setTimeout(function () {resolve({ ok: true, status: 200, json: function () { return Promise.resolve({ id: 1, name: "Alice" }); } }) — mirrors the shape of a real Response: ok, status, and a json() method that itself returns a Promise.}, 80); }); }console.log("Fetching user...")mockFetch("/api/user") — returns a Promise for the Response..then(function (response) { return response.json(); }) — json() returns another Promise for the parsed object; returning it chains the next then..then(function (data) { console.log("User:", data); })console.log("Request started") — real fetch works the same way: first Promise for Response, second for the body.Checking response.ok
fetch() only rejects on network failure. HTTP
error statuses (404, 500, etc.) still resolve. Always check
response.ok before treating the response as success.
What this code does: Shows how to detect an HTTP error by testing
response.ok and throwing when the status is not OK.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: false,
status: 404,
json: function () {
return Promise.resolve({ error: "Not found" });
}
});
}, 80);
});
}
console.log("Fetching missing resource...");
mockFetch("/api/missing")
.then(function (response) {
if (!response.ok) {
throw new Error("HTTP error: " + response.status);
}
return response.json();
})
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.log("Caught:", error.message);
});
console.log("Request started");
function mockFetch(url)ok: false, status: 404 — fetch resolves even on 404/500; it only rejects on network failure.console.log("Fetching missing resource...")mockFetch("/api/missing").then(function (response) {if (!response.ok) { throw new Error("HTTP error: " + response.status); } — converting a failed status into a rejection so the catch handler can deal with it.return response.json()}).then(function (data) { console.log(data); }) — skipped when we threw above..catch(function (error) { console.log("Caught:", error.message); })console.log("Request started") — always check response.ok; fetch will not reject for you on 404/500.Using async/await with Fetch
Because fetch() returns a promise, it works
naturally with async/await, which
often reads more clearly than a long .then() chain.
What this code does: Rewrites the basic fetch flow using
async/await for a cleaner sequential style.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: true,
status: 200,
json: function () {
return Promise.resolve({ id: 1, name: "Michael" });
}
});
}, 80);
});
}
async function getUser() {
console.log("Fetching with async/await...");
const response = await mockFetch("/api/user");
const data = await response.json();
console.log(data);
}
getUser();
console.log("getUser() called");
ok: true, status: 200 with a json() method returning user data.async function getUser() — await makes the two-step fetch/json pattern read top-to-bottom.console.log("Fetching with async/await...")const response = await mockFetch("/api/user")const data = await response.json() — second await for the parsed JSON.console.log(data)}getUser()console.log("getUser() called") — same two-promise structure as the .then version, just clearer to read.Error Handling with try…catch
With async/await, both network errors
and manually thrown HTTP errors are caught by the same
try...catch block.
What this code does: Combines async/await with
try...catch so both network failures and HTTP errors
are handled in one place.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: false,
status: 500,
json: function () {
return Promise.resolve({ error: "Server error" });
}
});
}, 80);
});
}
async function getUser() {
try {
console.log("Fetching...");
const response = await mockFetch("/api/user");
if (!response.ok) {
throw new Error("HTTP error: " + response.status);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.log("Error:", error.message);
}
}
getUser();
console.log("getUser() called");
ok: false, status: 500async function getUser()try {console.log("Fetching...")const response = await mockFetch("/api/user")if (!response.ok) { throw new Error("HTTP error: " + response.status); } — turns the HTTP error into an exception the catch block can handle.const data = await response.json()console.log(data)} catch (error) {console.log("Error:", error.message)} }getUser()console.log("getUser() called")Sending Data with a POST Request
To send data, pass a second options object to
fetch() with a method,
headers, and a body. The body is
usually a JSON string created with
JSON.stringify().
What this code does: Shows how to send a POST request by passing a second options object that contains the method, headers, and JSON body.
function mockFetch(url, options) {
return new Promise(function (resolve) {
setTimeout(function () {
console.log("Request method:", options.method);
console.log("Request body:", options.body);
resolve({
ok: true,
status: 201,
json: function () {
return Promise.resolve({ id: 2, created: true });
}
});
}, 80);
});
}
const newUser = {
name: "David",
age: 32
};
console.log("Sending POST...");
mockFetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newUser)
})
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log("Created:", data);
});
console.log("POST started");
function mockFetch(url, options)return new Promise(function (resolve) {setTimeout(function () {console.log("Request method:", options.method)console.log("Request body:", options.body)resolve({ ok: true, status: 201, json: ... })}, 80); }); }const newUser = { name: "David", age: 32 }console.log("Sending POST...")mockFetch("/api/users", {method: "POST"headers: { "Content-Type": "application/json" } — tells the server the body is JSON.body: JSON.stringify(newUser) — fetch does not auto-serialize objects; you must stringify.}).then(function (response) { return response.json(); }).then(function (data) { console.log("Created:", data); })console.log("POST started")Fetching Multiple Resources in Parallel
When several requests do not depend on each other, start them
all first, then use Promise.all() to wait for every
one of them together.
What this code does: Starts several independent requests at the same
time and waits for all of them with Promise.all().
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: true,
status: 200,
json: function () {
return Promise.resolve({ url: url });
}
});
}, 80);
});
}
async function getAllData() {
console.log("Fetching in parallel...");
const results = await Promise.all([
mockFetch("/api/users").then(function (r) { return r.json(); }),
mockFetch("/api/posts").then(function (r) { return r.json(); })
]);
console.log("Users:", results[0]);
console.log("Posts:", results[1]);
}
getAllData();
console.log("getAllData() called");
json: function () { return Promise.resolve({ url: url }); }async function getAllData()console.log("Fetching in parallel...")const results = await Promise.all([mockFetch("/api/users").then(function (r) { return r.json(); }) — starts immediately.mockFetch("/api/posts").then(function (r) { return r.json(); }) — also starts immediately; both run together.]) — waits only for the slower of the two.console.log("Users:", results[0])console.log("Posts:", results[1])}getAllData()console.log("getAllData() called") — prefer this over sequential awaits when the requests do not depend on each other.Canceling a Request with AbortController
An AbortController lets you cancel a fetch request
that is already in progress. The real fetch()
accepts a signal option; this mock recreates the
same behavior.
What this code does: Creates an AbortController, passes
its signal to the request, and aborts the request after a short
timeout so it never completes.
function mockFetch(url, options) {
return new Promise(function (resolve, reject) {
const timer = setTimeout(function () {
resolve({
ok: true,
status: 200,
json: function () {
return Promise.resolve({ url: url });
}
});
}, 300);
if (options && options.signal) {
options.signal.addEventListener("abort", function () {
clearTimeout(timer);
reject(new Error("Fetch aborted"));
});
}
});
}
const controller = new AbortController();
console.log("Starting abortable request...");
mockFetch("/api/slow", { signal: controller.signal })
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.log("Caught:", error.message);
});
setTimeout(function () {
controller.abort();
console.log("Abort signal sent");
}, 50);
function mockFetch(url, options)return new Promise(function (resolve, reject) {const timer = setTimeout(... resolve ... , 300)if (options && options.signal) {options.signal.addEventListener("abort", function () { clearTimeout(timer); reject(new Error("Fetch aborted")); }) — real fetch uses the same AbortSignal pattern.} }); }const controller = new AbortController() — owns a signal you can pass to fetch and later abort.console.log("Starting abortable request...")mockFetch("/api/slow", { signal: controller.signal }).then(... response.json ...).then(function (data) { console.log(data); }).catch(function (error) { console.log("Caught:", error.message); })setTimeout(function () { controller.abort(); console.log("Abort signal sent"); }, 50) — fires long before the 300 ms resolve, so the request is cancelled.Fetch API vs. XMLHttpRequest
| Feature | Fetch API | XMLHttpRequest |
|---|---|---|
| Based on promises? | Yes | No (event callbacks) |
| Works with async/await? | Yes | Not directly |
| Rejects on HTTP errors (404, 500)? | No — only on network failure | No — status must be checked |
| Built-in request cancellation | AbortController | xhr.abort() |
| Syntax | Modern, concise | Older, more verbose |
Common Mistakes with Fetch
- Forgetting that
fetch()does not reject on HTTP error statuses like404or500. - Forgetting to call
.json()(or another body method) to actually read the response data. - Forgetting a
.catch()ortry...catch, leaving network errors unhandled. - Forgetting to set
"Content-Type": "application/json"when sending a JSON body. - Forgetting to call
JSON.stringify()on the body of a request.
What this code does: Illustrates a typical mistake: not checking
response.ok before reading the body.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: false,
status: 404,
json: function () {
return Promise.resolve({ error: "Not found" });
}
});
}, 80);
});
}
console.log("Mistake demo (no ok check)...");
mockFetch("/api/missing")
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log("This runs even though it 404'd:", data);
});
console.log("Request started");
ok: false, status: 404console.log("Mistake demo (no ok check)...")mockFetch("/api/missing").then(function (response) { return response.json(); }) — because fetch resolved, this runs even though status is 404..then(function (data) { console.log("This runs even though it 404'd:", data); }) — the classic bug: silent failure treated as success.console.log("Request started") — always check response.ok before trusting the body.Best Practices for Fetch
- Always check
response.okbefore treating the response as successful. - Always handle both network errors (
.catch()/try...catch) and HTTP errors (response.ok). - Use
Promise.all()to fetch independent resources in parallel instead of one after another. - Use
AbortControllerto cancel requests that are no longer needed. - Set the correct
Content-Typeheader when sending a request body.
Quick Summary
fetch(url)returns a promise that resolves to aResponseobject.- Reading the body with
.json()or.text()returns another promise. fetch()only rejects on network failure; HTTP errors must be checked withresponse.ok.fetch()works naturally withasync/await.- Pass a second options object to send a
method,headers, andbody. - Use
Promise.all()for parallel requests andAbortControllerto cancel a request.
Try It Yourself
Modify the function below. Try changing ok to
false, adjusting the delay, or adding a second
mockFetch() call combined with
Promise.all().
What this code does: A complete practice function that fetches data,
checks response.ok, parses JSON, and uses
try...catch...finally for robust error handling.
function mockFetch(url) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve({
ok: true,
status: 200,
json: function () {
return Promise.resolve({ name: "Michael", role: "Developer" });
}
});
}, 80);
});
}
async function main() {
try {
console.log("Fetching...");
const response = await mockFetch("/api/user");
if (!response.ok) {
throw new Error("HTTP error: " + response.status);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.log("Error:", error.message);
} finally {
console.log("Request finished");
}
}
main();
console.log("main() called");
ok: true, status: 200 with user data.async function main()try {console.log("Fetching...")const response = await mockFetch("/api/user")if (!response.ok) { throw new Error("HTTP error: " + response.status); }const data = await response.json()console.log(data)} catch (error) {console.log("Error:", error.message)} finally {console.log("Request finished")} }main()console.log("main() called") — try setting ok: false to exercise the catch path.Tip: Remember thatfetch()only rejects on a network failure — always checkresponse.okfor HTTP errors, pair it comfortably withasync/await, and reach forPromise.all()orAbortControllerwhen requests need to run together or be canceled.
Understanding the Fetch API builds directly on promises and async/await, and is the foundation for working with real APIs, loading data into a page, and building modern web applications.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.