یادم می‌آید سال‌ها پیش، اولین باری که با XMLHttpRequest کار کردم، برای یک درخواست ساده‌ی GET، بیست خط کد نوشتم. با onreadystatechange، با بررسی readyState، با xhr.status، با try/catch دور تمامش. چند سال بعد که اولین بار fetch را دیدم، باور نکردم که همان کار با سه خط انجام شود. ولی درست همان روز فهمیدم که سادگی ظاهری fetch، فریبنده است: در پروژه‌ی اولی که با آن نوشتم، خطاهای HTTP را به‌عنوان موفقیت گرفتم چون فراموش کرده بودم که fetch روی کدهای ۴xx و ۵xx هم Promise موفق برمی‌گرداند. آن باگ کوچک، ساعت‌ها وقت گرفت تا ریشه‌اش را پیدا کنم. از آن روز، fetch API در جاوااسکریپت برای من نه یک جایگزین ساده برای XMLHttpRequest، بلکه یک مدل ذهنی کامل است که باید درست فهمیده شود تا در پروژه‌های واقعی قابل اعتماد باشد. در این مقاله، همان مسیری را می‌روم که امروز با تازه‌کارها طی می‌کنم: از سینتکس پایه تا مدیریت پاسخ، ارسال داده، AbortController، مدیریت خطا و الگوهای واقعی.

چرا fetch جایگزین XMLHttpRequest شد؟

اگر تازه با جاوااسکریپت آشنا می‌شوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد Promise در جاوااسکریپت را مرور کنید — چون fetch یکی از پرکاربردترین مثال‌های Promise در عمل است. برای درک اینکه چرا fetch جایگزین XMLHttpRequest شد، سه دلیل اصلی وجود دارد:

  • Promise-محور: برخلاف XMLHttpRequest که بر پایه‌ی callback بود، fetch یک Promise برمی‌گرداند. یعنی می‌توانید با .then زنجیره کنید یا با async/await در جاوااسکریپت بنویسید — هر دو، خوانایی کد را چند برابر بهبود می‌دهند.
  • سینتکس ساده‌تر: یک درخواست GET ساده، در XMLHttpRequest بیست خط بود؛ در fetch یک خط است.
  • استاندارد مدرن: fetch بخشی از APIهای استاندارد مرورگر است و در Service Workerها هم قابل استفاده است — چیزی که با XMLHttpRequest سخت یا غیرممکن بود. اصول این تفاوت در مفاهیم پایه جاوااسکریپت در بخش مدل ذهنی آمدنی آمده است.

ولی در پروژه‌های واقعی، سادگی ظاهری fetch می‌تواند گمراه‌کننده باشد. سه تفاوت رفتاری دارد که در XMLHttpRequest وجود نداشتند و در پروژه‌های واقعی باید درک شوند:

  • fetch روی کدهای خطا Reject نمی‌شود: برخلاف شهود، fetch روی ۴۰۴ یا ۵۰۰ هم Resolve می‌شود. باید صریحاً response.ok را بررسی کنید — همین یک تله، در پروژه‌های واقعی به باگ‌های پنهان منجر می‌شود.
  • fetch کوکی‌ها را به‌طور پیش‌فرض نمی‌فرستد: برای درخواست‌های cross-origin، باید credentials را صریحاً تنظیم کنید.
  • fetch توسط CORS محدود می‌شود: درخواست به دامنه‌ی دیگر، نیازمند هدرهای CORS در سمت سرور است. اصول این موضوع در امنیت API آمده است.
fetch، سینتکس ساده‌تری دارد ولی قراردادهای متفاوتی هم دارد؛ اگر قراردادها را نشناسید، سادگی‌اش به دام تبدیل می‌شود — چون خطاها بی‌صدا رد می‌شوند.

اولین درخواست: GET ساده

ساده‌ترین شکل درخواست GET، یک خط کد است:

fetch("https://api.example.com/users")
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((error) => console.error("Failed:", error));

یا با async/await (خواناتر برای اکثر توسعه‌دهندگان):

async function getUsers() {
    const response = await fetch("https://api.example.com/users");
    const data = await response.json();
    return data;
}

سه نکته‌ی مهم در همین چند خط که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • fetch دو مرحله دارد: اول یک await برای دریافت پاسخ سرور (شامل status و headers)، دوم یک await برای خواندن بدنه. این تفکیک، کلید درک رفتار fetch است.
  • response.json() هم Promise برمی‌گرداند: چرا؟ چون خواندن بدنه‌ی پاسخ، خودش یک عملیات آسنکرون است. در پروژه‌های واقعی، فراموش‌کردن این await دوم، منبع رایج خطاهاست.
  • برای پاسخ‌های غیر-JSON، متد مناسب انتخاب کنید: response.text() برای متن، response.blob() برای فایل باینری (تصویر، PDF).

Response: بررسی و خواندن داده

شیء Response که fetch برمی‌گرداند، چند ویژگی و متد مهم دارد که در پروژه‌های واقعی زیاد استفاده می‌کنم:

ویژگی / متدتوضیحکاربرد
response.oktrue اگر status بین ۲۰۰ و ۲۹۹ باشدبررسی موفقیت درخواست
response.statusکد وضعیت HTTP (۲۰۰، ۴۰۴، ۵۰۰)بررسی دقیق کد
response.statusTextمتن وضعیت (OK, Not Found)نمایش به کاربر
response.headersهدرهای پاسخخواندن Content-Type، Authorization
response.json()تبدیل بدنه به آبجکتپاسخ‌های JSON
response.text()بدنه به‌شکل متنپاسخ‌های متنی
response.blob()بدنه به‌شکل فایل باینریدانلود تصویر، PDF
response.clone()کپی پاسخ برای خواندن چندبارهلاگ + پردازش

الگوی درستی که در پروژه‌های واقعی به آن رسیده‌ام: قبل از خواندن بدنه، همیشه response.ok را چک کنید:

async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
}

این الگو، در پروژه‌های واقعی تفاوت بین یک کد قوی و یک کد شکننده است — چون خطاهای HTTP را از همان لایه‌ی اولیه به exception تبدیل می‌کند که با try/catch قابل مدیریت است.

ارسال داده با POST و PUT

برای ارسال داده به سرور، از متد POST (یا PUT/PATCH) استفاده می‌کنید و داده را در بدنه‌ی درخواست می‌گذارید:

async function createUser(userData) {
    const response = await fetch("/api/users", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(userData),
    });

    if (!response.ok) {
        throw new Error(`Failed: ${response.status}`);
    }

    return response.json();
}

await createUser({ name: "Ali", email: "ali@example.com" });

سه نکته‌ی مهم در ارسال داده که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • JSON.stringify اجباری است: بدنه‌ی درخواست باید رشته باشد. اگر آبجکت را مستقیم بدهید، fetch آن را به [object Object] تبدیل می‌کند. این تله در پروژه‌های واقعی زیاد دیده می‌شود.
  • Content-Type را حتماً تنظیم کنید: بدون آن، سرور نمی‌داند داده به چه فرمتی است و ممکن است خطای پارس بدهد.
  • متدهای HTTP را درست انتخاب کنید: POST برای ساخت، PUT برای جایگزینی کامل، PATCH برای به‌روزرسانی جزئی، DELETE برای حذف. اصول کامل این طراحی در اصول طراحی REST API آمده است.

الگوی DELETE و PATCH هم مشابه است، فقط بدنه ممکن است نداشته باشد یا کوتاه‌تر باشد:

// حذف بدون بدنه
await fetch(`/api/users/${id}`, { method: "DELETE" });

// به‌روزرسانی جزئی
await fetch(`/api/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email: "new@example.com" }),
});

Headers و تنظیمات درخواست

هدرها، اطلاعات جانبی درخواست را منتقل می‌کنند — مثل احراز هویت، نوع محتوا یا زبان. سه روش کار با هدرها در fetch:

۱) به‌شکل آبجکت ساده

fetch("/api/data", {
    headers: {
        "Authorization": `Bearer ${token}`,
        "Content-Type": "application/json",
        "Accept-Language": "fa",
    },
});

۲) با کلاس Headers

const headers = new Headers();
headers.append("Authorization", `Bearer ${token}`);
headers.append("Content-Type", "application/json");

fetch("/api/data", { headers });

مزیت این روش: می‌توانید هدرها را به‌شکل داینامیک اضافه یا حذف کنید — مفید در پروژه‌های بزرگ که هدرها از منابع مختلف می‌آیند.

۳) الگوی مشترک: هدر Authorization

در پروژه‌های واقعی که با احراز هویت کار می‌کنید، معمولاً یک تابع مشترک برای درخواست‌های احراز‌شده می‌نویسید:

async function apiRequest(url, options = {}) {
    const token = getToken();

    const defaultHeaders = {
        "Content-Type": "application/json",
        ...(token && { "Authorization": `Bearer ${token}` }),
    };

    const response = await fetch(url, {
        ...options,
        headers: {
            ...defaultHeaders,
            ...options.headers,
        },
    });

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

    return response.json();
}

این الگو در پروژه‌های واقعی، جلوی تکرار بی‌پایان هدرها در هر درخواست را می‌گیرد و مدیریت توکن را متمرکز می‌کند. اصول کامل احراز هویت با توکن در احراز هویت در REST API و JWT چیست و چه کاربردی در احراز هویت دارد آمده است.

در پروژه‌های واقعی، هدر Authorization شبیه کلید خانه است؛ آن را در هر درخواست تکرار نکنید — یک تابع مشترک بسازید و از آنجا مدیریتش کنید.

کار با JSON: رایج‌ترین سناریو

حدود ۹۰٪ درخواست‌های fetch در پروژه‌های واقعی، با JSON سروکار دارند. یک الگوی کامل و متمرکز که در پروژه‌ها به‌کار می‌برم:

const api = {
    async get(url) {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async post(url, data) {
        const response = await fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data),
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async put(url, data) {
        const response = await fetch(url, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data),
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
    },

    async delete(url) {
        const response = await fetch(url, { method: "DELETE" });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.status === 204 ? null : response.json();
    },
};

const users = await api.get("/api/users");
const newUser = await api.post("/api/users", { name: "Ali" });

این الگو، در پروژه‌های واقعی معمولاً به یک کلاینت مشترک تبدیل می‌شود که تمام درخواست‌های API از آن رد می‌شوند. مزایا: کد یکدست، مدیریت خطا در یک نقطه، و امکان افزودن لاگ یا احراز هویت در آینده بدون تغییر تمام فراخوانی‌ها.

مدیریت خطا: تله‌ی بزرگ fetch

مهم‌ترین تله‌ای که در پروژه‌های واقعی به آن برخورده‌ام و در ابتدای مقاله هم اشاره کردم: fetch روی کدهای HTTP خطا (۴xx و ۵xx) Reject نمی‌شود. سه لایه‌ی خطا در fetch وجود دارد که باید همه‌شان را مدیریت کنید:

لایه اول: خطای شبکه

try {
    const response = await fetch("/api/data");
} catch (error) {
    // خطای شبکه — DNS، قطع اتصال، CORS
    console.error("Network error:", error);
}

لایه دوم: خطای HTTP (کدهای ۴xx و ۵xx)

const response = await fetch("/api/data");

if (!response.ok) {
    // خطای HTTP — باید خودتان بررسی کنید
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

لایه سوم: خطای داده

const response = await fetch("/api/data");

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

try {
    const data = await response.json();
} catch (error) {
    // پاسخ معتبر نبود (مثلاً HTML به‌جای JSON)
    throw new Error("Invalid JSON response");
}

الگوی کامل که همه‌ی لایه‌ها را مدیریت می‌کند:

async function safeFetch(url, options = {}) {
    try {
        const response = await fetch(url, options);

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        return await response.json();
    } catch (error) {
        console.error(`Fetch failed for ${url}:`, error.message);
        throw error;
    }
}

اصول کامل مدیریت خطا در مدیریت خطا در جاوااسکریپت آمده است — و اگر با سمت سرور کار می‌کنید، امنیت API نشان می‌دهد که چطور پیام‌های خطا را طراحی کنید که هم به کاربر کمک کند و هم اطلاعات حساس را لو ندهد.

AbortController و لغو درخواست

گاهی نیاز دارید یک درخواست را قبل از اتمام لغو کنید — مثلاً وقتی کاربر سریع تایپ می‌کند و می‌خواهید فقط آخرین درخواست اجرا شود:

const controller = new AbortController();

fetch("/api/search", { signal: controller.signal })
    .then((response) => response.json())
    .then((data) => console.log(data))
    .catch((error) => {
        if (error.name === "AbortError") {
            console.log("Request was cancelled");
        } else {
            console.error(error);
        }
    });

// لغو درخواست
controller.abort();

الگوی واقعی: جستجوی زنده با لغو درخواست قبلی

let currentController = null;

async function search(query) {
    if (currentController) {
        currentController.abort();
    }

    currentController = new AbortController();

    try {
        const response = await fetch(`/api/search?q=${query}`, {
            signal: currentController.signal,
        });
        const data = await response.json();
        renderResults(data);
    } catch (error) {
        if (error.name !== "AbortError") {
            console.error(error);
        }
    }
}

این الگو، در پروژه‌های واقعی منبع اصلی بهبود تجربه‌ی جستجو است. بدون آن، اگر کاربر سریع تایپ کند، ده‌ها درخواست همزمان به سرور فرستاده می‌شود و پاسخ‌ها ممکن است به ترتیب اشتباه برسند. اگر با Event Delegation و debounce در رویدادها در جاوااسکریپت آشنا هستید، ترکیب AbortController با debounce، بهینه‌سازی‌ای است که در پروژه‌های واقعی بارها از آن نتیجه گرفته‌ام.

از ES2022، امکان AbortSignal.timeout(ms) هم اضافه شده که timeout ساده را ممکن می‌کند:

const response = await fetch("/api/slow", {
    signal: AbortSignal.timeout(5000),  // لغو بعد از ۵ ثانیه
});
در پروژه‌های واقعی، درخواست‌های بدون لغو، مثل مهمان‌هایی هستند که هرگز نمی‌روند؛ با AbortController، خودتان تصمیم می‌گیرید کدام مهمان بماند و کدام برود.

کوکی‌ها و credentials

یکی از تفاوت‌های مهم fetch با XMLHttpRequest، نحوه‌ی مدیریت کوکی‌ها است. به‌طور پیش‌فرض، fetch کوکی‌ها را فقط برای درخواست‌های same-origin می‌فرستد و برای cross-origin، نه:

// بدون کوکی — برای cross-origin
await fetch("https://api.other.com/data");

// با کوکی — برای cross-origin
await fetch("https://api.other.com/data", {
    credentials: "include",
});

سه مقدار ممکن برای credentials:

  • "same-origin" (پیش‌فرض): کوکی‌ها فقط برای درخواست‌های به همان دامنه فرستاده می‌شوند.
  • "include": کوکی‌ها حتی برای درخواست‌های cross-origin هم فرستاده می‌شوند.
  • "omit": کوکی‌ها هرگز فرستاده نمی‌شوند.

نکته‌ی مهم در پروژه‌های واقعی: اگر از credentials: "include" استفاده می‌کنید، سمت سرور هم باید هدرهای CORS درست را بفرستد — و Access-Control-Allow-Origin نمی‌تواند * باشد:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

اصول امنیتی این هدرها در هدرهای امنیتی HTTP با جزئیات آمده است.

CORS و درخواست‌های cross-origin

CORS (Cross-Origin Resource Sharing) یکی از پرتکرارترین خطاهایی است که در پروژه‌های واقعی با آن روبرو می‌شوم. وقتی از یک دامنه به دامنه‌ی دیگری درخواست می‌فرستید، مرورگر یک preflight request (درخواست OPTIONS) می‌فرستد تا ببیند آیا سرور اجازه می‌دهد یا نه:

// مرورگر خودکار یک درخواست OPTIONS می‌فرستد
fetch("https://api.other.com/data", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
});

پاسخ سرور باید شامل این هدرها باشد:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

سه نکته‌ی مهم در CORS که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • CORS یک موضوع سمت سرور است، نه سمت کلاینت: نمی‌توانید با تنظیمات fetch، CORS را دور بزنید. اگر سرور هدرها را نفرستد، هیچ راهی از سمت کلاینت وجود ندارد (به‌جز استفاده از پروکسی).
  • خطای CORS در کنسول گمراه‌کننده است: مرورگر فقط می‌گوید «CORS error» ولی علت دقیق (نبود هدر، روش اشتباه) را نمی‌گوید. باید در تب Network مرورگر، درخواست OPTIONS را ببینید و پاسخ سرور را بررسی کنید.
  • درخواست‌های ساده (Simple) preflight نمی‌خواهند: اگر روش GET/HEAD/POST باشد و فقط هدرهای ساده داشته باشید، مرورگر مستقیم درخواست می‌فرستد. ولی به‌محض اضافه‌کردن هدر سفارشی مثل Authorization یا Content-Type: application/json، preflight فعال می‌شود.

در پروژه‌های واقعی، اگر با CORS زیاد درگیر می‌شوید، معمولاً یا سرور شما درست تنظیم نشده یا از ساختار پروکسی معکوس استفاده نمی‌کنید. راه‌حل تمیز: به‌جای درخواست مستقیم به دامنه‌ی دیگر، از پروکسی سمت سرورِ خودتان استفاده کنید — که اصول آن در فایروال ابری در برابر فایروال سنتی در سطح شبکه هم بررسی شده است.

آپلود فایل و FormData

برای آپلود فایل، از FormData استفاده می‌کنید — و fetch به‌طور خودکار هدر Content-Type مناسب (multipart/form-data) را تنظیم می‌کند:

async function uploadFile(file) {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("description", "User avatar");

    const response = await fetch("/api/upload", {
        method: "POST",
        body: formData,  // بدون Content-Type — fetch خودش تنظیم می‌کند
    });

    return response.json();
}

// استفاده با input file
const input = document.querySelector("input[type=\"file\"]");
input.addEventListener("change", async (event) => {
    const file = event.target.files[0];
    const result = await uploadFile(file);
    console.log("Uploaded:", result);
});

نکته‌ی حیاتی در آپلود فایل: هدر Content-Type را دستی تنظیم نکنید. اگر "Content-Type": "multipart/form-data" بگذارید، fetch نمی‌تواند boundary مناسب را اضافه کند و سرور خطای پارس می‌دهد. اجازه دهید fetch خودش هدر را تنظیم کند — همان boundary که به‌طور خودکار اضافه می‌شود، کلید موفقیت آپلود است.

این الگو در پروژه‌های واقعی، در آپلود تصویر پروفایل، ضمیمه فرم یا فایل‌های چندرسانه‌ای بسیار پرکاربرد است. اگر با آپلود در PHP کار می‌کنید، اصول مکمل سمت سرور در نوشتن کد PHP امن آمده است.

الگوهای واقعی در پروژه‌ها

چند الگوی fetch که در پروژه‌های واقعی به ذهنیت ثابت من تبدیل شده‌اند:

الگوی اول: Retry با تأخیر نمایی

async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let attempt = 0; attempt < retries; attempt++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            return response;
        } catch (error) {
            if (attempt === retries - 1) throw error;
            await new Promise((resolve) =>
                setTimeout(resolve, 1000 * Math.pow(2, attempt))
            );
        }
    }
}

الگوی دوم: Timeout خودکار

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    return fetch(url, {
        ...options,
        signal: AbortSignal.timeout(timeout),
    });
}

الگوی سوم: صف درخواست‌ها با محدودیت همزمانی

class RequestQueue {
    constructor(limit = 5) {
        this.limit = limit;
        this.active = 0;
        this.queue = [];
    }

    async add(url, options = {}) {
        if (this.active >= this.limit) {
            await new Promise((resolve) => this.queue.push(resolve));
        }

        this.active++;
        try {
            return await fetch(url, options);
        } finally {
            this.active--;
            if (this.queue.length) {
                this.queue.shift()();
            }
        }
    }
}

const queue = new RequestQueue(3);

// همه‌ی درخواست‌ها از همین صف رد می‌شوند
const requests = urls.map((url) => queue.add(url));
const responses = await Promise.all(requests);

این الگو در پروژه‌های واقعی، از فشار آوردن همزمان به سرور جلوگیری می‌کند. الگوی مشابهی در وب اسکرپینگ با پایتون هم به‌کار رفته — چون کنترل همزمانی، مهارتی است که در هر زبان و هر پروژه‌ای ارزش دارد.

الگوی چهارم: کش ساده در localStorage

async function cachedFetch(url, ttl = 60000) {
    const cacheKey = `cache_${url}`;
    const cached = localStorage.getItem(cacheKey);

    if (cached) {
        const { data, timestamp } = JSON.parse(cached);
        if (Date.now() - timestamp < ttl) {
            return data;
        }
    }

    const response = await fetch(url);
    const data = await response.json();

    localStorage.setItem(cacheKey, JSON.stringify({
        data,
        timestamp: Date.now(),
    }));

    return data;
}

برای داده‌هایی که تغییر کمی دارند (مثل لیست دسته‌بندی‌ها)، این الگو در پروژه‌های واقعی، تعداد درخواست به سرور را به‌شدت کاهش می‌دهد. برای کش در سطح گسترده‌تر، اصول آن در بهترین افزونه‌های کش وردپرس آمده است — هرچند آن‌جا کش سمت سرور بررسی شده، ولی مفاهیم مشترک است.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی کد پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

  • فراموش کردن بررسی response.ok: مهم‌ترین اشتباه. fetch روی کدهای ۴xx و ۵xx هم Resolve می‌شود. باید همیشه response.ok را بررسی کنید تا خطاها به exception تبدیل شوند.
  • فراموش کردن await دوم: response.json() هم Promise برمی‌گرداند. اگر await نکنید، نتیجه یک Promise است نه داده.
  • تنظیم دستی Content-Type در آپلود فایل: در FormData، اجازه دهید fetch خودش هدر را تنظیم کند. تنظیم دستی، boundary را حذف می‌کند و سرور خطا می‌دهد.
  • نبود credentials: "include" برای cross-origin: اگر انتظار دارید کوکی‌های احراز هویت فرستاده شوند ولی این گزینه را تنظیم نکنید، درخواست بدون احراز هویت می‌رود.
  • فراموش کردن AbortController در جستجوی زنده: بدون لغو درخواست قبلی، ده‌ها درخواست موازی می‌رود و ترتیب پاسخ‌ها ممکن است اشتباه باشد.
  • نبود مدیریت خطای CORS: خطای CORS پیام گمراه‌کننده‌ای دارد و معمولاً در کنسول ساده دیده می‌شود. باید در تب Network، درخواست preflight را بررسی کنید.
  • استفاده از fetch بدون try/catch: اگر خطای شبکه رخ دهد، fetch Reject می‌شود. بدون try/catch، کد شما با Unhandled Promise Rejection مواجه می‌شود.
  • نبود timeout: بدون AbortSignal.timeout، یک درخواست ممکن است ساعت‌ها معلق بماند. تنظیم timeout، از انتظارهای بی‌پایان جلوگیری می‌کند.
  • مصرف حافظه در کش localStorage: localStorage محدودیت حجم دارد (معمولاً ۵ مگابایت). کش بدون پاکسازی، به خطای quota می‌رسد.
  • مقایسه‌ی response.status با رشته: کد وضعیت عدد است. response.status === "200" هیچ‌وقت true نمی‌شود — باید response.status === 200 باشد.
  • فراموش کردن JSON.stringify در POST: اگر آبجکت را مستقیم در body بگذارید، fetch آن را به [object Object] تبدیل می‌کند. حتماً JSON.stringify بزنید.
  • بازنویسی منطق مشترک در هر درخواست: به‌جای یک تابع مشترک apiFetch، در هر جا کد تکراری. این عادت در پروژه‌های بزرگ، به کابوس نگهداری تبدیل می‌شود.
  • عدم لاگ خطا در محیط تولید: فقط نمایش پیام به کاربر، بدون لاگ در Sentry یا سیستم لاگ. نتیجه: در روز بحران، نمی‌دانید کدام درخواست با چه خطایی مواجه شده است.
  • ترکیب نادرست fetch با جریان همگام: اگر منطق برنامه شما نیاز به ترتیب دارد، از await استفاده کنید نه .then بدون انتظار.
  • نبود مستندسازی الگوی خطا: اگر تیم شما یک تابع مشترک apiFetch دارد، مستندسازی رفتار خطا در آن، ساعت‌ها دیباگ را ذخیره می‌کند.

یک توصیه‌ی عملی از تجربه: در پروژه‌های جدید، یک تابع مشترک apiFetch بسازید که همه‌ی این لایه‌ها را مدیریت کند — بررسی response.ok، timeout، retry، لاگ خطا و JSON parsing. تمام درخواست‌ها از این تابع رد شوند. این یک تصمیم کوچک در روز اول، صدها باگ پنهان را در ماه ششم حذف می‌کند. اگر با سمت سرور هم کار می‌کنید، اصول کامل طراحی API را در اصول طراحی REST API و امنیت API مرور کنید — چون ارتباط سالم بین کلاینت و سرور، ترکیبی از این دو طرف است. اگر با وردپرس کار می‌کنید و می‌خواهید از REST API آن استفاده کنید، API چیست و چه کاربردی دارد نقطه‌ی شروع خوبی است.

سخن آخر

fetch API در جاوااسکریپت، از یک fetch("url") ساده شروع می‌شود ولی در پروژه‌های واقعی، به ستون فقرات ارتباط کلاینت با سرور تبدیل می‌شود. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، fetch روی کدهای HTTP خطا Reject نمی‌شود — بررسی response.ok یک الزام است، نه یک توصیه؛ دوم، fetch دو مرحله دارد — دریافت پاسخ و خواندن بدنه — و فراموش‌کردن await دوم، منبع رایج باگ‌هاست؛ سوم، AbortController و timeout دو ابزار ضروری برای پروژه‌های واقعی هستند — درخواست بدون لغو و timeout، هم منابع را هدر می‌دهد و هم تجربه‌ی کاربری را خراب می‌کند.

اگر امروز می‌خواهید در fetch ماهر شوید، سه کار کوچک پیشنهاد می‌کنم: یک تابع مشترک apiFetch بسازید که بررسی response.ok، timeout و try/catch داشته باشد و همه‌ی درخواست‌های پروژه از آن رد شوند؛ یک جستجوی زنده پیاده کنید که با AbortController درخواست قبلی را لغو کند؛ و یک POST با هدر Authorization و JSON.stringify بنویسید و در تب Network مرورگر، درخواست و پاسخ را ببینید. همین سه تمرین، ۹۰٪ مهارت‌های عملی fetch را در ذهن شما زنده می‌کند. مسیر طبیعی بعدی، async/await در جاوااسکریپت، مدیریت خطا در جاوااسکریپت و احراز هویت در REST API است. اگر تجربه‌ای از کار با fetch در پروژه‌های خودتان دارید — مخصوصاً اگر با تله‌ی response.ok یا خطای CORS روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🔗