همه چیز درباره AbortController جاوااسکریپت

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

قبل از AbortController چطور کنسل می‌کردیم؟

قبل‌ترها، جاوااسکریپت هیچ راه استانداردی برای کنسل کردن یه Promise یا یه درخواست fetch نداشت. مردم مجبور بودن با یه پرچم (flag) دستی کار کنن، یعنی یه متغیر بولین می‌ساختن و وقتی جواب فچ می‌رسید چک می‌کردن که آیا اون درخواست هنوز معتبره یا نه. کار می‌کرد، ولی درخواست همچنان تا آخر روی شبکه ادامه پیدا می‌کرد و فقط نتیجه‌ش دور ریخته می‌شد؛ یعنی پهنای باند و منابع سرور بی‌خودی هدر می‌رفت. AbortController این مشکل رو به شکل استاندارد و تمیز حل کرد.

AbortController دقیقاً چیه؟

AbortController یه Web API بومیه که دو تا چیز بهت می‌ده:

  • controller: همون چیزیه که کنترلش دست توئه. با متد abort() روش، می‌گی «بس کن».
  • signal: یه شیء از نوع AbortSignal که به عملیات موردنظر (مثلاً fetch) پاس می‌دی. این signal فقط گوش می‌ده و وقتی controller دستور abort بده، خبردار می‌شه.

ساختنش خیلی ساده‌ست:

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

console.log(signal.aborted); // false

نکته‌ی جالب اینه که AbortSignal از روی EventTarget ساخته شده، یعنی دقیقاً مثل یه دکمه یا هر المان DOM دیگه می‌تونی روش event listener بذاری:

signal.addEventListener('abort', () => {
  console.log('عملیات لغو شد');
});

controller.abort();
console.log(signal.aborted); // true

مثال ساده: کنسل کردن یه fetch

کافیه signal رو داخل آبجکت تنظیمات fetch پاس بدی. وقتی abort() صدا زده بشه، پرامیس fetch با یه خطا reject می‌شه؛ نوع این خطا یه DOMException با نام AbortError هست.

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
  .then((res) => res.json())
  .then((data) => console.log('داده گرفته شد:', data))
  .catch((err) => {
    if (err.name === 'AbortError') {
      console.log('درخواست قبل از تموم شدن کنسل شد');
    } else {
      console.error('یه خطای دیگه پیش اومد:', err);
    }
  });

// هر جای دیگه از کد، هر وقت لازم بود:
controller.abort();

یه نکته‌ی مهم: کنسل کردن باعث نمی‌شه درخواست از روی شبکه پاک بشه. اگه تا اون لحظه از مرورگر بیرون رفته باشه، ممکنه سرور همچنان بهش رسیدگی کنه. چیزی که تغییر می‌کنه اینه که کد جاوااسکریپتت دیگه منتظر جواب نمی‌مونه و پرامیس رو reject می‌کنه.

یه نکته‌ی مهم: هر signal فقط یه بار قابل استفاده‌ست

بعد از اینکه یه controller رو abort کردی، دیگه نمی‌تونی از همون signal برای درخواست بعدی استفاده کنی؛ چون از همون لحظه همیشه aborted: true می‌مونه و هر fetch جدیدی که باهاش صدا بزنی، فوراً با AbortError رد می‌شه.

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

fetch('/another', { signal: controller.signal }).catch((err) => {
  console.log(err.name); // بلافاصله: AbortError
});

پس برای هر درخواست جدید، باید یه AbortController تازه بسازی. اینم دقیقاً همون الگوییه که توی مثال بعدی می‌بینی.

کاربرد واقعی: کنسل کردن درخواست‌های قدیمی توی سرچ باکس

یکی از رایج‌ترین جاهایی که این قابلیت به دردت می‌خوره، همون باکس سرچه. هر بار که کاربر تایپ می‌کنه، اول درخواست قبلی رو کنسل کن و بعد یه controller تازه بساز:

let currentController = null;

function runSearch(query) {
  if (currentController) {
    currentController.abort();
  }

  currentController = new AbortController();

  fetch(`/search?term=${encodeURIComponent(query)}`, {
    signal: currentController.signal,
  })
    .then((res) => res.json())
    .then((data) => console.log('نتایج سرچ:', data))
    .catch((err) => {
      if (err.name === 'AbortError') {
        console.log('یه درخواست قدیمی کنسل شد');
      } else {
        console.error('مشکلی توی سرچ پیش اومد:', err);
      }
    });
}

با این کار، همیشه فقط آخرین درخواست اجازه داره تا آخر بره جلو و نتیجه‌های قدیمی و بی‌ربط هیچ‌وقت روی UI نمی‌شینن.

استفاده توی ری‌اکت: کنسل کردن روی unmount

توی فریمورک‌هایی مثل React یا Vue، وقتی یه کامپوننت از بین می‌ره (مثلاً کاربر صفحه رو عوض می‌کنه)، درخواست fetchی که داخلش زده بودی خودش به خودی خود متوقف نمی‌شه. اگه جوابش بعد از رفتن کامپوننت برسه و بخوای state رو آپدیت کنی، مرورگر بهت وارنینگ می‌ده. راه‌حلش اینه که توی تابع cleanup مربوط به useEffect، همون controller رو abort کنی:

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

  fetch(`/user/${userId}`, { signal: controller.signal })
    .then((res) => res.json())
    .then((data) => setUser(data))
    .catch((err) => {
      if (err.name === 'AbortError') {
        console.log('کامپوننت قبل از رسیدن جواب، آنمانت شد');
      }
    });

  return () => {
    controller.abort();
  };
}, [userId]);

این‌طوری هر وقت userId عوض بشه یا کامپوننت از صفحه بره، درخواست قبلی خودکار کنسل می‌شه و دیگه نگران آپدیت کردن یه state روی یه کامپوننت مرده نیستی. اگه از کتابخونه‌هایی مثل React Query یا TanStack Query استفاده می‌کنی، خبر خوب اینه که خودشون زیر پوشش از یه signal استفاده می‌کنن و لازم نیست دستی این کار رو انجام بدی؛ کافیه همون signal که از پارامتر queryFn بهت می‌رسه رو به fetch پاس بدی.

ست کردن تایم‌اوت خودکار برای fetch

جاوااسکریپت به‌صورت پیش‌فرض هیچ تایم‌اوتی برای fetch نداره؛ اگه سرور کند باشه یا شبکه قطع و وصل بشه، درخواست ممکنه تا ابد معلق بمونه. با ترکیب setTimeout و AbortController می‌تونی خودت یه سقف زمانی تعریف کنی:

function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();

  const timerId = setTimeout(() => controller.abort(), timeout);

  return fetch(url, { signal: controller.signal }).finally(() => {
    clearTimeout(timerId);
  });
}

اگه جواب زودتر از تایم‌اوت برسه، تایمر پاک می‌شه و همه چیز عادیه. اگه دیرتر برسه، تایمر خودش abort رو صدا می‌زنه و fetch با AbortError متوقف می‌شه. استفاده از finally هم مطمئن می‌شه که تایمر همیشه پاک‌سازی بشه، چه fetch موفق بشه چه نشه.

AbortController فقط برای fetch نیست

هر تیکه کدی که بخواد به signal گوش بده، می‌تونه از این مکانیزم استفاده کنه؛ یعنی می‌تونی تایمرها، حلقه‌های طولانی یا هر عملیات async دلخواهت رو هم قابل‌کنسل کنی. مثلاً یه تابع delay که قابل لغو باشه:

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);

    signal.addEventListener(
      'abort',
      () => {
        clearTimeout(id);
        reject(new DOMException('زودتر متوقف شد', 'AbortError'));
      },
      { once: true }
    );
  });
}

const controller = new AbortController();

delay(3000, controller.signal)
  .then(() => console.log('۳ ثانیه صبر کردیم'))
  .catch((err) => {
    if (err.name === 'AbortError') console.log('تایمر کنسل شد');
  });

setTimeout(() => controller.abort(), 1000);

یعنی هر جای برنامه‌ت که یه پروسه‌ی زمان‌بر داری، فقط کافیه به signal.aborted چک کنی یا روی رویداد abort گوش بدی و خودت تصمیم بگیری چطور تمیزش کنی.

چند نکته و تله‌ی رایج

  • هر AbortController فقط یک‌بار مصرفه؛ برای هر عملیات جدید، یه نمونه‌ی تازه بساز.
  • کنسل کردن یه فچ، لزوماً درخواست رو از روی شبکه برنمی‌داره؛ فقط جاوااسکریپتت دیگه منتظر جوابش نمی‌مونه.
  • همیشه توی catch چک کن که آیا err.name === 'AbortError' هست یا نه، تا اون رو به‌عنوان یه خطای واقعی به کاربر نشون ندی.
  • موقع کار با useEffect توی ری‌اکت، فراموش نکن که controller رو توی تابع cleanup صدا بزنی.

جمع‌بندی

خلاصه‌ی ماجرا اینه: یه AbortController می‌سازی، signalش رو به هر عملیاتی که می‌خوای قابل‌کنسل باشه پاس می‌دی، و هر وقت لازم شد controller.abort() رو صدا می‌زنی. همین یه مکانیزم ساده، مشکل درخواست‌های قدیمی، لیک حافظه توی کامپوننت‌های آنمانت‌شده و تایم‌اوت‌های دستی رو یک‌جا حل می‌کنه؛ بدون اینکه لازم باشه خودت یه سیستم پیچیده برای مدیریتش بسازی.

منابع