شاید یادتان باشد که در آن «جهنم کالبکها» یا همان هرم مرگ، مجبور بودیم تابع failureCallback را ۳ بار تکرار کنیم! اما در زنجیره پرامیسها، ما فقط و فقط یکبار آن را در انتهای زنجیره مینویسیم:
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
در این حالت، اگر در هر کجای زنجیره یک استثنا (Exception) یا خطا رخ دهد، مرورگر کل زنجیره را به سمت پایین طی میکند تا به متد .catch() یا مدیریتکنندههای خطای onRejected برسد. این رفتار دقیقاً از روی نحوه کارکرد کدهای همگام (Synchronous) الگوبرداری شده است؛ به این کدهای همگام نگاه کنید:
try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
این هماهنگی و تقارن بینظیر بین کدهای ناهمگام و همگام، در ساختار مدرن async/await به اوج خود میرسد:
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch (error) {
failureCallback(error);
}
}
پرامیسها با گرفتن تمام خطاها (حتی خطاهای ناشی از throw شدن استثناها و خطاهای برنامهنویسی)، یکی از بزرگترین عیبهای هرم کالبکها را برطرف کردهاند. این ویژگی برای ترکیب و سرهم کردن درستِ عملیاتهای ناهمگام حیاتی است. حالا تمام خطاها در انتهای زنجیره توسط متد .catch() مدیریت میشوند و شما تقریباً هیچوقت نیازی به استفاده از try/catch معمولی نخواهید داشت، مگر اینکه بخواهید از async/await استفاده کنید.
در مثالهایی که بالاتر درباره لیست مواد اولیه (listOfIngredients) با هم دیدیم، در حالت اول یک زنجیره پرامیس در دل مقدار بازگشتیِ یک .then() دیگر قرار گرفته بود (تودرتو)، اما در حالت دوم زنجیره کاملاً صاف و یکدست بود. به عنوان یک قاعده کلی، بهتر است زنجیرههای پرامیس ساده را به صورت صاف و بدون تودرتویی نگه داریم؛ چون تودرتو کردنِ بیمورد معمولاً نتیجه بیدقتی در چیدن کدهاست.
با این حال، تودرتویی یک ابزار کنترل ساختار است که به ما اجازه میدهد قلمرو (Scope) دستورات catch را محدود کنیم. به زبان سادهتر، یک .catch() تودرتو و داخلی، فقط و فقط خطاهای مربوط به قلمروی خودش و لایههای پایینترش را میگیرد، نه خطاهای مراحل بالاتر در زنجیره اصلی را! اگر از این ویژگی درست استفاده کنیم، میتوانیم بازیابی و مدیریت خطاها را با دقت بسیار بالاتری انجام دهیم:
doSomethingCritical()
.then((result) =>
doSomethingOptional(result)
.then((optionalResult) => doSomethingExtraNice(optionalResult))
.catch((e) => {}), // اگر کارهای اختیاری خراب شد، نشنیده بگیر و به کار ادامه بده!
)
.then(() => moreCriticalStuff())
.catch((e) => console.error(`Critical failure: ${e.message}`));
📌 یک نکته ظریف: توجه داشته باشید که مراحل اختیاری در اینجا تودرتو شدهاند. این تودرتویی به خاطر تو رفتگی کدها (Indentation) نیست، بلکه به خاطر پرانتزهای
(و)بیرونی است که دور این مراحل گذاشتهایم.
در این کد، متد داخلیِ .catch() که خطاها را سایلنت میکند، فقط خطاهای ناشی از doSomethingOptional() و doSomethingExtraNice() را پوشش میدهد. بعد از این مراحل، کد دوباره مسیر عادیاش را از بخش moreCriticalStuff() از سر میگیرد. نکته مهم اینجاست که اگر همان مرحله اول یعنی doSomethingCritical() شکست بخورد، خطای آن فقط و فقط توسط کچ نهایی (بیرونی) گرفته میشود و کچ داخلی اصلاً دستش به آن نمیرسد تا آن را مخفی کند.
اگر بخواهیم همین کد تودرتو را با ساختار async/await بنویسیم، این شکلی میشود:
async function main() {
try {
const result = await doSomethingCritical();
try {
const optionalResult = await doSomethingOptional(result);
await doSomethingExtraNice(optionalResult);
} catch (e) {
// خطاهای مراحل اختیاری را نادیده بگیر و به مسیرت ادامه بده.
}
await moreCriticalStuff();
} catch (e) {
console.error(`Critical failure: ${e.message}`);
}
}
💡 یادآوری: اگر برنامه شما نیازی به مدیریت خطاهای پیچیده و تفکیکشده ندارد، به احتمال خیلی زیاد اصلاً نیازی به کدهای تودرتو (Nested then handlers) ندارید. در این مواقع بهتر است زنجیره را صاف نگه دارید و منطق مدیریت خطا را به همان انتهای زنجیره بسپارید.
شما میتوانید حتی بعد از یک شکست (یعنی بعد از یک متد .catch()) هم زنجیره را ادامه بدهید! این کار برای زمانی مفید است که میخواهید حتی در صورت خراب شدن یک کار، اقدامات جدیدی را در طول زنجیره انجام دهید. به این مثال نگاه کنید:
doSomething()
.then(() => {
throw new Error("Something failed");
console.log("Do this"); // این خط هیچوقت اجرا نمیشود
})
.catch(() => {
console.error("Do that");
})
.then(() => {
console.log("Do this, no matter what happened before");
});
با اجرای کد بالا، متن زیر در خروجی چاپ میشود:
Do that
Do this, no matter what happened before
(نکته: عبارت "Do this" چاپ نشد، چون خطای "Something failed" باعث رد شدن یا Rejection پرامیس شد و کد مستقیم پرید روی کچ).
همین منطق در ساختار async/await به این صورت پیادهسازی میشود:
async function main() {
try {
await doSomething();
throw new Error("Something failed");
console.log("Do this");
} catch (e) {
console.error("Do that");
}
console.log("Do this, no matter what happened before");
}
اگر یک پرامیس رد (Reject) شود اما هیچ کدی (مثل catch) برای مدیریت آن وجود نداشته باشد، این خطا در پشته فراخوانی (Call Stack) به سمت بالا حرکت میکند تا به بالاترین سطح برسد و اینجاست که محیط اجرا (مرورگر یا نودجیاس) باید آن را آشکار کند.
در مرورگر، هر زمان که یک پرامیس رد شود و رها گردد، یکی از دو رویداد زیر به قلمروی سراسری (که معمولاً window است یا در وبورکرها، محیط Worker) ارسال میشود:
unhandledrejection: زمانی ارسال میشود که یک پرامیس رد شده، اما هیچ کد یا مدیریتکنندهای برای این رد شدن در دسترس نیست.
rejectionhandled: زمانی ارسال میشود که یک پرامیس از قبل رد شده و رویداد unhandledrejection را هم تولید کرده، اما حالا یک مدیریتکننده (مثل catch) به آن متصل شده است.
در هر دو حالت، این رویدادها (که از نوع PromiseRejectionEvent هستند) دو عضو مهم دارند: ویژگی promise (که نشان میدهد کدام پرامیس رد شده) و ویژگی reason (که علت یا همان پیام رد شدن پرامیس را نشان میدهد).
این رویدادها به ما اجازه میدهند یک لایه بکآپ و سراسری برای مدیریت خطاهای پرامیسها داشته باشیم و راحتتر کدهایمان را عیبیابی (Debug) کنیم. از آنجا که این هندلرها در کل محیط به صورت سراسری (Global) عمل میکنند، تمام خطاهای بدون مدیریت به همینجا ختم میشوند؛ فرقی هم نمیکند منبع خطا کجا باشد.
در محیط Node.js، قضیه کمی متفاوت است. شما برای کپچر کردن پرامیسهای مدیریتنشده، باید یک شنونده (Handler) برای رویداد اختصاصی نودجیاس یعنی unhandledRejection ثبت کنید (به بزرگ بودن حرف R در نام رویداد دقت کنید)، چیزی شبیه به این:
process.on("unhandledRejection", (reason, promise) => {
// کدهای شما برای بررسی مقدار "promise" و "reason" اینجا قرار میگیرد
});
در Node.js، برای اینکه مانع چاپ شدن خودکار خطا در کنسول شوید (رفتار پیشفرضی که خود نودجیاس دارد)، فقط کافی است همین لیسنر process.on() را اضافه کنید و نیازی به متدهایی مثل preventDefault() مرورگر ندارید.
اما حواستان باشد: اگر این لیسنر را اضافه کنید ولی داخل آن هیچ کدی برای بررسی و مدیریت واقعی پرامیسهای رد شده ننوشته باشید، خطاها اصطلاحاً روی زمین میافتند و بدون اینکه بفهمید نادیده گرفته میشوند! بنابراین، بهترین کار این است که حتماً داخل این تابع، کدی برای بررسی دقیق هر پرامیس بنویسید تا مطمئن شوید خطا به خاطر یک باگ واقعی در کدهایتان نبوده باشد.
هر زمان که مایل بودید، بخش بعدی مستندات را ارسال کنید تا کار را جلو ببریم!
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript