زنجیره‌ی Promise و مدیریت خطا در مسیر پردازش ناهمزمان

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

ساخت یک زنجیره‌ی then

هر فراخوانی then خودش یک Promise جدید برمی‌گرداند؛ به همین دلیل می‌توان چندین then را پشت سر هم زنجیره کرد. مقدار بازگشتی هر then، ورودی then بعدی می‌شود:

fetch("/api/user/5")
  .then((response) => response.json())
  .then((user) => fetch(`/api/orders?userId=${user.id}`))
  .then((response) => response.json())
  .then((orders) => {
    console.log("سفارش‌های کاربر:", orders);
  });

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

انتشار خطا در طول زنجیره

یکی از مزیت‌های اصلی Promise نسبت به Callback ساده، نحوه‌ی مدیریت خطاست. اگر در هر نقطه از زنجیره خطایی رخ دهد—چه با reject شدن یک Promise و چه با throw شدن یک خطا—اجرای بقیه‌ی thenها به‌طور خودکار متوقف شده و کنترل مستقیماً به نزدیک‌ترین catch منتقل می‌شود:

انتشار خطا در زنجیره‌ی Promise
خروجی کنسول اینجا نمایش داده می‌شود...

نکته‌ی کلیدی این است که فقط یک catch در انتهای زنجیره کافی است تا خطای هر یک از مراحل قبلی—چه در اولین fetch و چه در آخرین then—را دریافت کند. این رفتار مشابه بلوک try/catch در کدهای Synchronous است که در درس‌های قبل با آن آشنا شدید.

ادامه‌ی زنجیره پس از catch

یکی از نکاتی که کمتر مورد توجه قرار می‌گیرد این است که catch نیز خودش یک Promise برمی‌گرداند و زنجیره را کاملاً متوقف نمی‌کند، مگر آنکه دوباره خطا throw شود:

fetch("/api/data")
  .then((response) => response.json())
  .catch((error) => {
    console.log("خطا رخ داد، مقدار پیش‌فرض استفاده می‌شود");
    return { items: [] }; // مقدار جایگزین برای ادامه‌ی زنجیره
  })
  .then((data) => {
    console.log("ادامه‌ی پردازش با داده:", data);
  });

در این الگو، اگر خطایی رخ دهد، catch آن را مدیریت کرده و با بازگرداندن یک مقدار جایگزین، اجازه می‌دهد زنجیره به‌جای متوقف‌شدن کامل، با یک مقدار پیش‌فرض ادامه پیدا کند.

finally: اجرای کد صرف‌نظر از نتیجه

متد finally یک بلوک کد تعریف می‌کند که چه زنجیره موفق شود و چه با خطا مواجه شود، همیشه اجرا می‌شود؛ مناسب برای کارهایی مثل پنهان‌کردن یک نشانگر بارگذاری:

showLoadingSpinner();

fetch("/api/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.log("خطا:", error.message))
  .finally(() => {
    hideLoadingSpinner(); // این خط در هر دو حالت موفقیت و خطا اجرا می‌شود
  });

جدول رفتار متدهای زنجیره‌ی Promise

متد زمان اجرا
then پس از resolve شدن موفق Promise قبلی
catch پس از reject شدن یا throw شدن خطا در هر نقطه از زنجیره‌ی قبلی
finally در هر دو حالت موفقیت و خطا، صرف‌نظر از نتیجه

نکات کلیدی

  • هر then یک Promise جدید برمی‌گرداند و همین ویژگی امکان زنجیره‌کردن چند عملیات ناهمزمان را فراهم می‌کند.
  • یک catch در انتهای زنجیره، خطای هر یک از مراحل قبلی را می‌گیرد، نه فقط آخرین then.
  • catch خودش نیز Promise برمی‌گرداند؛ با بازگرداندن یک مقدار جایگزین، زنجیره می‌تواند پس از خطا نیز ادامه پیدا کند.
  • finally صرف‌نظر از موفقیت یا شکست زنجیره، همیشه اجرا می‌شود و برای کارهایی مثل مخفی‌کردن نشانگر بارگذاری مناسب است.

جمع‌بندی

زنجیره‌کردن Promise با then امکان نوشتن مراحل متوالی از عملیات ناهمزمان را فراهم می‌کند، بدون افتادن در دام تودرتویی Callbackهای متعدد. مزیت اصلی این الگو نسبت به Callback ساده، مدیریت متمرکز خطا با یک catch در انتهای زنجیره است که مشابه try/catch در کد Synchronous عمل می‌کند. در درس بعدی، ابزارهایی مثل Promise.all برای اجرای هم‌زمان چند Promise مستقل بررسی خواهد شد.