frontend

كيفية إلغاء طلب Fetch في JavaScript (2026)

٢٩ يونيو ٢٠٢٦

How to Cancel a Fetch Request in JavaScript (2026)

لإلغاء طلب fetch، قم بإنشاء AbortController، ومرر الـ signal الخاص به إلى fetch(url, { signal })، ثم استدعِ controller.abort() عندما تريد التوقف. سيتم رفض الـ promise الخاص بالطلب بـ AbortError يمكنك تجاهله بأمان. في React، استدعِ abort() من وظيفة التنظيف (cleanup) الخاصة بـ useEffect.

ملخص

إلغاء طلب fetch يعتمد على أداة قياسية واحدة في الويب: AbortController. أنشئ وحدة تحكم (controller)، ومرر controller.signal إلى fetch، واستدعِ controller.abort() لإيقاف الطلب — سيتم رفضه حينها بـ DOMException يسمى AbortError. بالنسبة للمهلات الزمنية (timeouts)، تخطَّ رقصة setTimeout القديمة واستخدم AbortSignal.timeout(ms)، الذي يرفض الطلب بـ TimeoutError؛ ادمج مهلة زمنية مع زر إلغاء باستخدام AbortSignal.any([...]). في React، أنشئ وحدة التحكم داخل useEffect واستدعِ abort() في وظيفة التنظيف لقتل الطلبات القديمة وتجنب ظروف السباق (race conditions). تم التحقق من سلوكيات AbortController الأساسية، والمهلة الزمنية، و AbortSignal.any() الموضحة هنا مقابل خادم حي على Node.js 22.

ما ستتعلمه

  • كيفية إلغاء طلب fetch باستخدام AbortController و signal
  • كيفية اكتشاف وتجاهل AbortError الذي يطلقه الإلغاء بأمان
  • كيفية إلغاء fetch عند إلغاء تثبيت المكون (unmount) وتجنب ظروف السباق في React useEffect
  • لماذا يتم إطلاق fetch مرتين في بيئة التطوير، ومتى يجب القلق بشأن ذلك
  • كيفية إضافة مهلة زمنية للطلب باستخدام AbortSignal.timeout()
  • كيفية دمج مهلة زمنية وزر إلغاء باستخدام AbortSignal.any()
  • لماذا يُستخدم AbortController لمرة واحدة فقط، وكيفية إعادة استخدام النمط بشكل صحيح
  • المتصفحات وبيئات التشغيل التي تدعم هذه الواجهات البرمجية (APIs)
  • كيف تتعامل axios و TanStack Query مع الإلغاء

كيف تلغي طلب fetch في JavaScript؟

أنشئ AbortController، ومرر الـ signal الخاص به إلى fetch، واستدعِ abort() عندما تريد إيقاف الطلب. AbortController هو الطريقة القياسية والمدمجة لجعل fetch قابلاً للإلغاء — لا حاجة لمكتبات خارجية.1

// Create a controller and grab its signal
const controller = new AbortController();

fetch("/API/search?q=React", { signal: controller.signal })
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => {
    if (err.name === "AbortError") {
      console.log("Request was cancelled");
    } else {
      throw err; // a real network or parsing error
    }
  });

// Later — cancel it
controller.abort();

عند استدعاء abort()، يتم رفض الـ promise الخاص بـ fetch() بـ DOMException يسمى AbortError.1 الـ signal هو الجانب المخصص للقراءة فقط من وحدة التحكم الذي تسلمه لـ fetch؛ بينما يحتفظ الـ controller بزر الـ abort(). يمكنك تمرير نفس الإشارة (signal) لعدة استدعاءات fetch، واستدعاء abort() واحد سيلغيها جميعاً في وقت واحد — وهو أمر مفيد عندما يؤدي إجراء مستخدم واحد إلى إطلاق مجموعة من الطلبات.

كيف تتعامل مع AbortError بعد إلغاء fetch؟

قم بالتقاط الرفض (rejection) والتحقق من err.name === "AbortError". الإلغاء هو شيء قمت أنت بتفعيله عن قصد، لذا فإن الخطوة المعتادة هي تجاهل هذا الخطأ المحدد وإعادة إطلاق (re-throw) أي شيء آخر. نسخة async/await تبدو أكثر وضوحاً:

const controller = new AbortController();

try {
  const res = await fetch(url, { signal: controller.signal });
  const data = await res.json();
  // use data
} catch (err) {
  if (err.name === "AbortError") return; // expected — do nothing
  throw err; // anything else is a genuine failure
}

إذا كنت تريد معرفة سبب إلغاء الطلب، فقم بتمرير سبب إلى abort(). أي قيمة تمررها تصبح هي سبب الرفض ويتم تخزينها أيضاً في signal.reason:

controller.abort(new Error("User navigated away"));

استدعاء controller.abort(new Error("User navigated away")) يجعل الـ fetch يرفض باستخدام كائن الـ Error هذا بالضبط — وهو ما تم التحقق منه عن طريق الهوية في الاختبار — بدلاً من الـ AbortError الافتراضي.1 يمكنك أيضاً حماية الكود الذي لا ينبغي تشغيله بمجرد إلغاء الإشارة باستخدام signal.throwIfAborted()، والتي تطلق السبب المخزن إذا كانت الإشارة قد أُلغيت بالفعل ولا تفعل شيئاً بخلاف ذلك.1

كيف تلغي طلب fetch في React باستخدام useEffect؟

أنشئ الـ AbortController داخل الـ effect، ومرر الـ signal الخاص به إلى fetch، وأعد وظيفة تنظيف (cleanup function) تستدعي controller.abort(). يقوم React بتشغيل وظيفة التنظيف تلك عند إلغاء تثبيت المكون وقبل إعادة تشغيل الـ effect، لذا يتم إلغاء الطلب القديم قبل بدء طلب جديد.2

import { useEffect, useState } from "React";

function SearchResults({ query }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function load() {
      try {
        const res = await fetch(`/API/search?q=${query}`, {
          signal: controller.signal,
        });
        setData(await res.json());
      } catch (err) {
        if (err.name !== "AbortError") throw err;
      }
    }
    load();

    return () => controller.abort(); // cancel on unmount or when query changes
  }, [query]);

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

هذا يحل خطأين حقيقيين. الأول هو ظرف السباق (race condition): إذا تغيرت الـ query من "alice" إلى "bob"، فقد يصل استجابة "alice" الأبطأ في النهاية وتكتب فوق "bob". إلغاء الطلب القديم يجعل ذلك مستحيلاً. الثاني هو النمط الكلاسيكي "لا يمكن تحديث الحالة في مكون غير مثبت"، لأن الطلب الملغى لا يصل أبداً إلى setData.

تقدم وثائق React نمطاً ثانياً — وهو علامة ignore — يمكنك استخدامه بدلاً من الإلغاء، أو بجانبه:2

useEffect(() => {
  let ignore = false;
  fetch(`/API/search?q=${query}`)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setData(data);
    });
  return () => {
    ignore = true;
  };
}, [query]);

الفرق ملموس: علامة ignore تتجاهل الاستجابة القديمة ولكن طلب الشبكة يكتمل، بينما AbortController يقوم فعلياً بإلغاء الطلب ويوفر عرض النطاق الترددي (bandwidth). الإلغاء هو الخيار الأقوى عندما تكون الاستجابات كبيرة أو مكلفة.

لماذا يتم تشغيل fetch مرتين أو إلغاؤه فوراً في React؟

بسبب <StrictMode> في بيئة التطوير. يقوم React عمداً باستدعاء الـ effects مرتين في بيئة التطوير — الإعداد (setup)، ثم التنظيف (cleanup)، ثم الإعداد مرة أخرى — لمساعدتك في اكتشاف عمليات التنظيف المفقودة.3 مع وجود تنظيف AbortController، فهذا يعني أن طلبك الأول يتم إلغاؤه فوراً تقريباً، لذا قد ترى طلباً إضافياً و AbortError (أو إدخال "canceled" في علامة تبويب الشبكة Network) أثناء التطوير.

هذا أمر متوقع ويكون في بيئة التطوير فقط. في بيئة الإنتاج، يتم تشغيل الـ effect مرة واحدة ولا يتم إلغاء أي شيء قبل الأوان. حقيقة أنك ترى الإلغاء تثبت أن وظيفة التنظيف الخاصة بك تعمل — وهذا ليس خطأ يجب "إصلاحه" عن طريق حذف StrictMode أو وظيفة التنظيف. وبما أن المثال أعلاه يتجاهل بالفعل AbortError، فإن الطلب المزدوج لا يسبب أي مشكلة مرئية للمستخدمين.

كيف تضيف مهلة زمنية لطلب fetch؟

مرر AbortSignal.timeout(ms) كـ signal. لا يحتوي معيار Fetch على خيار timeout مدمج، لذا فإن هذه الطريقة الثابتة (static method) هي الطريقة الحديثة والرسمية لإعطاء الطلب موعداً نهائياً.4 يتم إلغاء الإشارة تلقائياً بعد عدد المللي ثانية المحدد ويرفض الـ fetch بـ TimeoutError DOMException.5

التفصيلة الأساسية هي اسم الخطأ. تنتهي المهلة (timeout) برفض يحمل الاسم TimeoutError، بينما الإلغاء الذي يطلبه المستخدم يرفض بـ AbortError، لذا يمكن لكتلة catch واحدة التمييز بينهما — وهو ما تم التحقق منه بتشغيل مهلة 50 مللي ثانية مقابل نقطة نهاية تستغرق ثانية واحدة.5 هذا التمييز مهم: فانتهاء المهلة يستحق عادةً رسالة "حاول مرة أخرى"، بينما الإلغاء المتعمد يستحق الصمت عادةً. (ملاحظة تاريخية: المتصفحات القائمة على Chromium قبل إصدار Chrome 124 كانت تبلغ عن انتهاء مهلة fetch كـ AbortError بدلاً من TimeoutError؛ المتصفحات الحالية تتبع المواصفات.6) هذا يحل محل النمط القديم المتمثل في إنشاء AbortController، وبدء setTimeout يستدعي abort()، ومسح المؤقت يدوياً.

كيف تجمع بين مهلة زمنية وزر إلغاء؟

استخدم AbortSignal.any() لدمج عدة إشارات في إشارة واحدة. تأخذ مصفوفة من الإشارات وتعيد إشارة يتم إلغاؤها بمجرد إلغاء أي منها — وهي مثالية لسيناريو "الإلغاء بعد 10 ثوانٍ أو عندما ينقر المستخدم على إلغاء".7

const controller = new AbortController();
cancelButton.addEventListener("click", () => controller.abort());

const res = await fetch(url, {
  signal: AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]),
});

تتبنى الإشارة المدمجة سبب الإلغاء الخاص بأي إشارة مصدر يتم إلغاؤها أولاً.7 هناك ملاحظة تستحق المعرفة: تشير MDN إلى أنه، على عكس AbortSignal.timeout() المنفردة، لا يمكنك دائماً الاعتماد على اسم الخطأ وحده لمعرفة ما إذا كان الإلغاء المدمج ناتجاً عن انتهاء المهلة.1 عندما تحتاج إلى معرفة السبب بيقين، تحقق من أي وحدة تحكم (controller) تم تفعيلها بدلاً من اسم الخطأ:

} catch (err) {
  if (controller.signal.aborted) {
    console.log("Cancelled by the user");
  } else {
    console.log("Request timed out");
  }
}

تكون قيمة controller.signal.aborted هي true فقط عندما يتم تشغيل معالج الإلغاء الخاص بك، لذا فهي تفصل بين الإلغاء اليدوي وانتهاء المهلة بشكل موثوق عبر المحركات المختلفة. في كلتا الحالتين، تحصل على إلغاء متعدد الطبقات — مهلة زمنية و يدوي — من إشارة signal واحدة، دون الحاجة للتعامل مع المؤقتات بنفسك.

هل يمكنك إعادة استخدام AbortController لطلبات متعددة؟

لا — إن AbortController (وإشارته) مخصص للاستخدام لمرة واحدة فقط. كما تذكر MDN، الإشارة "يمكن استخدامها مرة واحدة فقط"؛ بعد إلغائها، يتم رفض أي fetch يستخدم نفس الإشارة فوراً.1 في الاختبارات، أدى تمرير إشارة ملغاة بالفعل إلى طلب fetch جديد إلى رفضه فوراً بـ AbortError.

لذا القاعدة هي: أنشئ AbortController جديداً لكل عملية منطقية قد ترغب في إلغائها. ما يمكن لوحدة تحكم واحدة فعله هو تغطية عدة طلبات تريد إلغاءها معاً — مرر إشارة واحدة لعدة استدعاءات fetch وسيؤدي استدعاء abort() واحد إلى إيقافها جميعاً. إذا كنت بحاجة لإلغائها بشكل مستقل، فامنح كل منها وحدة تحكم خاصة بها.

// One controller per request you might cancel on its own
function startRequest(url) {
  const controller = new AbortController();
  const promise = fetch(url, { signal: controller.signal });
  return { promise, cancel: () => controller.abort() };
}

const { promise, cancel } = startRequest("/API/report");
// cancel(); // stops just this request

هل AbortController مدعوم في جميع المتصفحات و Node.js؟

تعتبر ميزات AbortController و AbortSignal و fetch القابل للإلغاء من ميزات Baseline "المتاحة على نطاق واسع"؛ تدرج MDN أن AbortController متاح عبر المتصفحات منذ مارس 2019، لذا فإن نمط الإلغاء الأساسي آمن للاستخدام في كل مكان اليوم.8 المساعدان الجديدان أحدث عهداً: تدرج MDN أن AbortSignal.timeout() ضمن Baseline 2024 (متاح منذ أبريل 2024) و AbortSignal.any() ضمن Baseline 2024 (متاح منذ مارس 2024).57

إذا كان عليك دعم متصفحات قديمة تفتقر إلى AbortSignal.timeout()، فإن الحل البديل الكلاسيكي لا يزال يعمل: أنشئ AbortController، وابدأ setTimeout يستدعي controller.abort()، وامسح المؤقت بمجرد اكتمال الطلب. هذه الواجهات البرمجية موجودة أيضاً في بيئات تشغيل الخادم الحديثة — Node.js و Deno و Bun — لذا فإن نفس كود الإلغاء يعمل على الخادم.

هل يقوم axios و TanStack Query بإلغاء الطلبات بنفس الطريقة؟

نعم — كلاهما يعتمد على نفس معيار AbortController، لذا فإن المفهوم ينتقل مباشرة. مع axios، تقوم بتمرير إشارة AbortController في إعدادات الطلب؛ تم إيقاف دعم CancelToken القديم في الإصدار v0.22.0 (أكتوبر 2021) ويجب أن تفضل نهج الإشارة.9

const controller = new AbortController();
axios.get("/API/search", { signal: controller.signal });
controller.abort();

مع TanStack Query، لا تقوم عادةً بإنشاء وحدة تحكم على الإطلاق: تمرر المكتبة AbortSignal إلى دالة الاستعلام الخاصة بك وتلغيها تلقائياً عندما يصبح الاستعلام قديماً أو غير نشط. ما عليك سوى تمرير تلك الإشارة إلى fetch.10

useQuery({
  queryKey: ["todos"],
  queryFn: async ({ signal }) => {
    const res = await fetch("/API/todos", { signal });
    return res.json();
  },
});

الخلاصة

إلغاء عملية fetch هو مهمة تعتمد على أداة واحدة: AbortController للإلغاء اليدوي، و AbortSignal.timeout() للمواعيد النهائية، و AbortSignal.any() للجمع بينهما — وكلها معايير قياسية، مدمجة، ومدعومة في المتصفحات وبيئات التشغيل الحديثة. في React، يختصر النمط بالكامل في "إنشاء متحكم داخل useEffect، وإلغاؤه في دالة التنظيف (cleanup)"، مما يقضي على الطلبات القديمة وحالات السباق (race conditions) التي تصاحبها.

للتعمق أكثر في الأسس غير المتزامنة (async) التي يقوم عليها كل هذا، راجع كيفية عمل الـ callbacks والـ promises والـ async/await ودليلنا حول أنماط الـ async الحديثة في JavaScript. وإذا كنت تفضل ترك مكتبة تتعامل مع الإلغاء، والتخزين المؤقت (caching)، وحالات السباق نيابة عنك، فراجع كيف تدير TanStack Query الطلبات والتحديثات المتفائلة (optimistic updates).

Footnotes

  1. MDN Web Docs — AbortSignal interface (abort behavior, single-use signals, reason, throwIfAborted). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal 2 3 4 5 6 7 8 9

  2. React — Synchronizing with Effects (fetch cleanup, ignore vs. abort, race conditions). https://React.dev/learn/synchronizing-with-effects 2 3

  3. React — <StrictMode> (re-runs Effects an extra time in development). https://React.dev/reference/React/StrictMode 2

  4. WHATWG Fetch Standard — "Add a timeout option, to prevent hanging" (issue #951; fetch has no built-in timeout option). https://GitHub.com/whatwg/fetch/issues/951 2

  5. MDN Web Docs — AbortSignal: timeout() static method (TimeoutError on timeout, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static 2 3 4

  6. mdn/browser-compat-data issue #20381 — AbortSignal.timeout() surfaced AbortError instead of TimeoutError in Chromium before Chrome 124. https://GitHub.com/mdn/browser-compat-data/issues/20381

  7. MDN Web Docs — AbortSignal: any() static method (combine signals, first-reason wins, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static 2 3 4

  8. MDN Web Docs — AbortController interface (Baseline "widely available," across browsers since March 2019). https://developer.mozilla.org/en-US/docs/Web/API/AbortController

  9. Axios — Cancellation (AbortController signal; CancelToken deprecated since v0.22.0). https://axios-http.com/docs/cancellation

  10. TanStack Query — إلغاء الاستعلام (AbortSignal يتم تمريره إلى دالة الاستعلام). https://tanstack.com/query/latest/docs/framework/React/guides/query-cancellation

الأسئلة الشائعة

أنشئ AbortController ، ومرر controller.signal إلى fetch(url, { signal }) ، واستدعِ controller.abort() للإلغاء. عندها يتم رفض وعد (promise) الـ fetch بـ DOMException يسمى AbortError . 1