, ,

5 Common Mistakes Developers Make When Using the Fetch API

Posted by

The Fetch API looks simple, but it hides gotchas that break real-world apps. Here’s how to avoid the mistakes most developers still make.

The Fetch API looks simple, but it hides gotchas that break real-world apps. Here’s how to avoid the mistakes most developers still make.

Introduction

If you’ve ever written:

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

…it works fine until it doesn’t.

Maybe your response isn’t JSON.
Maybe the request never ends.
Maybe the server returns 500, but your app says everything’s fine.

The Fetch API is modern, powerful, and native, but it’s also trickier than it looks.

Let’s go through 5 common mistakes developers make (and how to fix them) so your Fetch code stays reliable in production.


Mistake #1: Assuming Fetch Throws on HTTP Errors

This is the most common misunderstanding.

❌ Wrong

try {
const res = await fetch("/api/users");
const data = await res.json();
} catch (err) {
console.error("Request failed:", err);
}

You expect this to throw for 404 or 500, but it doesn’t.
fetch() Only rejects for network-level issues (like DNS or offline).

✅ Right

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

try {
const data = await safeFetch("/api/users");
console.log(data);
} catch (err) {
console.error(err.message);
}

💡 Always check response.ok to detect bad HTTP statuses.


Mistake #2: Forgetting Timeouts

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

✅ Fix with AbortController

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

try {
const res = await fetch(url, { signal: controller.signal });
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;
} finally {
clearTimeout(timeout);
}
}

✅ Prevents “infinite loading” issues.
✅ Let’s you cancel pending requests cleanly.


Mistake #3: Sending the Wrong Request Body

This one’s sneaky. Developers often forget to stringify data or set proper headers.

❌ Wrong

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

The backend receives [object Object].

✅ Right

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

💡 Always set Content-Type and JSON.stringify() when sending JSON data.


Mistake #4: Not Handling Non-JSON Responses

Not every API returns JSON.
If you blindly call .json() on an HTML or text response boom, runtime error.

✅ Always check Content-Type

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

let data;
if (contentType && contentType.includes("application/json")) {
data = await res.json();
} else {
data = await res.text();
}

console.log("Response:", data);

✅ Safe for any API
✅ No surprises when the backend changes


Mistake #5: Rewriting Fetch Logic Everywhere

This one’s about consistency.
Instead of writing new Fetch logic in every file, wrap it once.

✅ Build a helper function

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();
}

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

✅ Centralized error handling
✅ Reusable, maintainable
✅ Works for any request type


Bonus Handle Retries Gracefully

Network blips happen.
Add a simple retry wrapper to keep your UX smooth.

async function fetchWithRetry(url, options = {}, retries = 2) {
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 request...");
return fetchWithRetry(url, options, retries - 1);
}
throw err;
}
}

Why These Mistakes Matter

Fetch isn’t broken, it’s just lower level than you think.
If you don’t handle timeouts, headers, or HTTP errors yourself, your app will feel unreliable.

Understanding Fetch deeply gives you the same control Axios or SWR provides but natively, with zero dependencies.

✅ Fewer bugs
✅ Predictable network behavior
✅ Simpler debugging and testing


Conclusion

The Fetch API is deceptively simple, but mastering it separates beginner JavaScript devs from real-world engineers.

Key takeaways:

  1. Always check response.ok.
  2. Add timeouts with AbortController.
  3. Always stringify JSON bodies.
  4. Check response type before parsing.
  5. Centralize your Fetch logic.

Once you do this, your API layer will be fast, predictable, and bulletproof.

“It’s not about using Fetch it’s about using it correctly.”


Call to Action

Have you ever been bitten by a Fetch bug that took hours to de

bug?
Drop your story in the comments 👇

And if your teammate still uses .then(res => res.json()) Without checks, send them this post before their next deploy.

Leave a Reply

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