یکی از اصلیترین کاربردهای قابلیت مدیریت منابع، تضمین این نکته است که منابع همیشه و در هر شرایطی آزاد شوند، حتی زمانی که در برنامه خطایی رخ میدهد. بیا با هم سناریوهای پیچیدهتر مدیریت خطا را بررسی کنیم.
کار را با این کد شروع میکنیم که به لطف استفاده از دستور using، در برابر خطاها کاملاً مقاوم و پایدار است:
async function readUntil(stream, text) {
// استفاده از using به جای await using چون متد releaseLock همگام (sync) است
using reader = stream.getReader();
let chunk = await reader.read();
while (!chunk.done && chunk.value !== text) {
console.log(chunk.toUpperCase());
chunk = await reader.read();
}
}
تصور کنید مقدار chunk در یک مرحله null از آب دربیاید. در این صورت، خط کد chunk.toUpperCase() یک خطای نوع یا همان TypeError پرتاب میکند که باعث متوقف شدن فوری تابع میشود. اما نکته جادویی اینجاست: درست قبل از اینکه تابع کاملاً خارج شود، متد [Symbol.dispose]() مربوط به استریم خودکار صدا زده میشود و قفل استریم را آزاد میکند.
const stream = new ReadableStream({
start(controller) {
controller.enqueue("a");
controller.enqueue(null); // این مقدار نال باعث بروز خطا میشود
controller.enqueue("b");
controller.enqueue("c");
controller.close();
},
});
readUntil(stream, "b")
.catch((e) => console.error(e)) // خروجی: TypeError: chunk.toUpperCase is not a function
.then(() => {
const anotherReader = stream.getReader();
// با موفقیت یک Reader جدید میسازد و استریم قفل نیست!
});
پس متوجه میشویم که دستور using اصلاً خطاها را قورت نمیدهد یا پنهان نمیکند؛ تمام خطاهایی که رخ میدهند کماکان پرتاب میشوند، اما منابع دقیقاً یک ثانیه قبل از پرتاب خطا بسته میشوند.
بیا یک مثال کمی عجیب اما واقعی را بررسی کنیم؛ فرض کنید خودِ متدِ پاکسازی منبع هم با خطا مواجه شود:
class MyReader {
[Symbol.dispose]() {
throw new Error("Failed to release lock"); // خطای بخش پاکسازی
}
}
function doSomething() {
using reader = new MyReader();
throw new Error("Failed to read"); // خطای بدنه اصلی تابع
}
try {
doSomething();
} catch (e) {
console.error(e); // خروجی: SuppressedError: An error was suppressed during disposal
}
در جریان اجرای تابع doSomething() دو خطای مجزا تولید میشود:
خطای اول در بدنه اصلی تابع رخ میدهد (Failed to read).
خطای دوم در زمان تلاش برای پاکسازی reader رخ میدهد (Failed to release lock) که خود این پاکسازی به خاطر رخ دادن خطای اول تحریک شده بود.
از آنجا که هر دو خطا همزمان اتفاق افتادهاند و جاوااسکریپت باید هر دو را به شما گزارش کند، چیزی که در بلوک catch دریافت میکنید یک شیء ویژه به نام SuppressedError است. این یک خطای مخصوص است که دو خطا را در دل خود کپسوله (بستهبندی) میکند:
ویژگی e.error: حاوی خطای بعدی (جدیدتر) است (یعنی خطایی که حین پاکسازی رخ داده).
ویژگی e.suppressed: حاوی خطای قبلی (قدیمیتر) است که توسط خطای جدیدتر «سرکوب» شده است (یعنی همان خطای اصلی بدنه تابع).
اگر بیشتر از یک منبع داشته باشیم و همهی آنها در حین پاکسازی خطا پرتاب کنند (که پدیدهای بسیار بسیار نادر است، چون خراب شدن خودِ فرآیند پاکسازی به خودی خود نادر است!)، آنوقت هر خطای قبلی توسط خطای بعدی سرکوب میشود و یک زنجیره پیوسته از خطاهای سرکوبشده را تشکیل میدهد.
بیا این چالش زنجیرهای را در کد زیر کالبدشکافی کنیم:
class MyReader {
[Symbol.dispose]() {
throw new Error("Failed to release lock on reader");
}
}
class MyWriter {
[Symbol.dispose]() {
throw new Error("Failed to release lock on writer");
}
}
function doSomething() {
using reader = new MyReader();
using writer = new MyWriter();
throw new Error("Failed to read"); // خطای اول (اصلی)
}
try {
doSomething();
} catch (e) {
console.error(e); // SuppressedError
console.error(e.suppressed); // SuppressedError
// دسترسی به تکتک خطاها در زنجیره:
console.error(e.error); // Error: Failed to release lock on reader
console.error(e.suppressed.suppressed); // Error: Failed to read
console.error(e.suppressed.error); // Error: Failed to release lock on writer
}
اگر یادتان باشد، گفتیم منابع با ترتیب معکوس (از آخر به اول) آزاد میشوند. پس فرآیند به این صورت جلو میرود:
منبع writer آخرین منبعی بود که تعریف شد، پس اول از همه تلاش میکند تا آزاد شود. حین آزاد شدن خطا میدهد. این خطای و رایتر، خطای اصلی تابع (Failed to read) را سرکوب میکند.
منبع reader اولین منبعی بود که تعریف شد، پس آخر از همه تلاش میکند آزاد شود. حین آزاد شدن خطا میدهد. چون این خطا آخرین و جدیدترین اتفاق کل این مسیر است، سرکوبکننده نهایی نام میگیرد و کل پکیج قبلی را سرکوب میکند!
در نتیجه در بلوک catch:
خطای نهایی و بیرونیترین خطا (e.error) مربوط به reader است، چون آخر از همه اجرا شد.
خطای میانی (e.suppressed.error) مربوط به writer است.
خطای اول و ریشهایترین خطا (e.suppressed.suppressed) همان خطای اصلی برنامه یعنی Failed to read است.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript