, ,

Most Developers Still Don’t Use the Fetch API the Right Way

Posted by

It’s 2025, yet most codebases still misuse Fetch by omitting error checks and implementing broken retries. Here’s how to actually do it right.

It’s 2025, yet most codebases still misuse Fetch from missing error checks to broken retries. Here’s how to actually do it right.

Introduction

You’ve seen this line a thousand times:

fetch("/api/data").then((res) => res.json()).then(console.log);

Looks fine, right?
Except it silently fails on 404s, swallows timeouts, and floods logs when the network hiccups.

The Fetch API is powerful but also deceptively simple.
Most developers use only 20% of what it can actually do.

This guide fixes that.
We’ll cover everything you’re probably doing wrong and how to fix it so your Fetch code finally behaves the way you think it does.


Mistake #1: Assuming Fetch Throws on HTTP Errors

❌ The wrong assumption

try {
const res = await fetch("/api/users");
const data = await res.json(); // crashes if 404 page isn’t JSON
} catch (err) {
console.log("Error:", err);
}

You’d expect this to throw on 404 or 500, right? Nope.
Fetch only rejects the promise on network failures (DNS issues, offline, etc.), not bad HTTP statuses.

✅ The right way

async function safeFetch(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}

💡 Rule: Always check response.ok before touching .json().


Mistake #2: Ignoring Timeouts

By default, Fetch waits forever. If your API hangs, your app hangs too.

✅ Fix with AbortController

async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);

try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === "AbortError") throw new Error("Request timed out");
throw err;
}
}

✅ Stops hanging calls
✅ Works perfectly with async/await
✅ Gives you control over retries


Mistake #3: Forgetting to Handle Retries

APIs fail, and network blips are normal.
But most devs never retry a Fetch.

✅ Simple retry wrapper

async function fetchWithRetry(url, options = {}, retries = 3) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
if (retries > 0) {
console.warn(`Retrying... (${3 - retries + 1})`);
return fetchWithRetry(url, options, retries - 1);
}
throw err;
}
}

✅ Recovers from temporary network issues
✅ Keeps UX smooth without user refreshes


Mistake #4: Using .then() Chains Instead of async/await

You can spot legacy code instantly:

fetch(url)
.then((res) => res.json())
.then((data) => doSomething(data))
.catch(console.error);

Readable? Not really.

✅ Modern approach

try {
const res = await fetch(url);
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}

Cleaner, debuggable, and fits perfectly with async frameworks like React or Next.js.


Mistake #5: Not Setting Headers Correctly

Most devs forget to send headers when posting JSON:

❌ Wrong

fetch("/api/users", {
method: "POST",
body: { name: "Umar" }, // not JSON!
});

✅ Correct

fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Umar" }),
});

💡 Tip: Always stringify() JSON bodies and declare the correct Content-Type.


Mistake #6: Forgetting About Abort (User Cancels, Page Changes)

If a user navigates away mid-request, Fetch keeps running in the background.

✅ Use AbortController Again, it’s not just for timeouts.

const controller = new AbortController();
const signal = controller.signal;

fetch("/api/search?q=javascript", { signal });

// cancel when user closes search
controller.abort();

This is how React Query, SWR, and modern frameworks safely cancel network calls.


Mistake #7: Not Handling Non-JSON Responses

Not every endpoint returns JSON.

✅ Always check Content-Type:

const res = await fetch("/data");
const type = res.headers.get("content-type");

if (type.includes("application/json")) {
const json = await res.json();
} else {
const text = await res.text();
}

Prevents runtime crashes when APIs return HTML or plain text on error pages.


Mistake #8: Not Creating a Reusable Fetch Wrapper

Instead of writing fetch() everywhere, make one helper that handles all the common stuff:

async function api(url, options = {}) {
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
...options.headers,
},
...options,
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}

Now you can call:

await api("/api/posts");
await api("/api/posts", { method: "POST", body: JSON.stringify({ title: "Hello" }) });

✅ Consistent behavior
✅ Fewer bugs
✅ Easier maintenance


Mistake #9: Forgetting About Parallel Fetches

You don’t need to wait for each request if they’re independent.

const [users, posts] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
]);

✅ Saves seconds in dashboards or data-heavy views.


Mistake #10: Using Fetch Without Understanding CORS

CORS (Cross-Origin Resource Sharing) still trips up devs daily.
The fix? Know what’s happening:

  • Browser blocks requests to other origins by default.
  • The server must send headers like:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT

💡 You can’t fix CORS from the frontend; it’s a server configuration issue.


🧩 Bonus Handling Streams and Large Responses

When downloading large files, read the stream progressively:

const res = await fetch("/large-data");
const reader = res.body.getReader();
let received = 0;

while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
console.log("Received bytes:", received);
}

✅ Efficient for large downloads
✅ Doesn’t block UI or memory


Why Most Developers Still Get It Wrong

Because Fetch seems simple but hides complex behavior:

  • Doesn’t reject on 404
  • Doesn’t timeout
  • Doesn’t cancel automatically
  • Doesn’t parse all content types

Understanding these quirks makes your app reliable instead of “works most of the time.”

Fetch is not broken developers just stop at the tutorial level.


Conclusion

You don’t need Axios, Superagent, or third-party wrappers; you just need to know how to use Fetch properly.

✅ Always check res.ok
✅ Add timeout and retry logic
✅ Handle non-JSON responses
✅ Reuse a single Fetch wrapper

Once you do, your network layer becomes bulletproof, simple, native, and modern.

Master Fetch, and you master 90% of web communication.


Call to Action

Have you ever been bitten by a silent Fetch failure or a missing timeout?
Drop your “Fetch horror story” in the comments 👇

And if your teammate still writesfetch(...).then(...).then(...), share this post; they’ll finally understand how to use it the right way.

Leave a Reply

Your email address will not be published. Required fields are marked *