پیش‌بینی گرفتن در Chrome DevTools: چرا سخت است و چگونه آن را بهتر کنیم

اریک لیز
Eric Leese

اشکال زدایی استثناها در برنامه های کاربردی وب ساده به نظر می رسد: زمانی که مشکلی پیش می آید اجرای را متوقف کنید و بررسی کنید. اما ماهیت ناهمزمان جاوا اسکریپت این را به طرز شگفت آوری پیچیده می کند. چگونه می‌تواند Chrome DevTools بداند زمانی که استثناها از طریق وعده‌ها و توابع ناهمزمان عبور می‌کنند، چه زمانی و کجا مکث کند؟

این پست به چالش‌های پیش‌بینی شکار می‌پردازد – توانایی DevTools برای پیش‌بینی اینکه آیا یک استثنا بعداً در کد شما ثبت می‌شود یا خیر. ما بررسی خواهیم کرد که چرا اینقدر مشکل است و چگونه پیشرفت‌های اخیر در V8 (موتور جاوا اسکریپت که کروم را تامین می‌کند) آن را دقیق‌تر می‌کند و منجر به تجربه اشکال‌زدایی روان‌تر می‌شود.

چرا پیش‌بینی مهم است

در Chrome DevTools، گزینه‌ای دارید که اجرای کد را فقط برای استثناهای کشف نشده متوقف کنید، و از مواردی که دستگیر شده‌اند رد شوید.

Chrome DevTools گزینه‌های جداگانه‌ای را برای مکث در موارد استثنا شده یا کشف نشده ارائه می‌کند

در پشت صحنه، هنگامی که یک استثنا برای حفظ متن رخ می دهد، اشکال زدا بلافاصله متوقف می شود. این یک پیش‌بینی است، زیرا در حال حاضر نمی‌توان مطمئن شد که آیا استثنا بعداً در کد ثبت می‌شود یا نه، به خصوص در سناریوهای ناهمزمان. این عدم قطعیت از دشواری ذاتی پیش‌بینی رفتار برنامه، مشابه مشکل توقف ، ناشی می‌شود.

مثال زیر را در نظر بگیرید: کجا باید دیباگر مکث کند؟ (در بخش بعدی به دنبال پاسخ باشید.)

async function inner() {
  throw new Error(); // Should the debugger pause here?
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ... or should the debugger pause here?
  }
}

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

پیش بینی های نادرست منجر به ناامیدی می شود:

  • منفی های کاذب (پیش بینی "غیر گیر" زمانی که گرفتار می شود) . توقف های غیر ضروری در دیباگر.
  • موارد مثبت کاذب (پیش‌بینی "گرفتار" زمانی که کشف نشود) . فرصت‌های از دست رفته برای دریافت خطاهای مهم، به طور بالقوه شما را مجبور می‌کند همه استثناها، از جمله موارد مورد انتظار را اشکال‌زدایی کنید.

روش دیگر برای کاهش وقفه‌های اشکال‌زدایی، استفاده از فهرست نادیده گرفته می‌شود، که از شکستن استثناها در کد شخص ثالث مشخص شده جلوگیری می‌کند. با این حال، پیش‌بینی دقیق صید هنوز در اینجا بسیار مهم است. اگر استثنایی که از کد شخص ثالث منشأ می گیرد فرار کرد و بر روی کد شما تأثیر گذاشت، باید بتوانید آن را اشکال زدایی کنید.

نحوه عملکرد کدهای ناهمزمان

Promises، async و await و سایر الگوهای ناهمزمان می‌توانند به سناریوهایی منجر شوند که در آن یک استثنا یا رد، قبل از رسیدگی، ممکن است مسیر اجرایی را طی کند که در زمان ایجاد استثنا، تعیین آن دشوار است. این به این دلیل است که ممکن است منتظر وعده‌ها نباشند یا تا زمانی که استثنا رخ داده باشد، کنترل‌کننده‌های catch اضافه شوند. بیایید به مثال قبلی خود نگاه کنیم:

async function inner() {
  throw new Error();
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ...
  }
}

در این مثال، outer() ابتدا inner() را فراخوانی می کند که بلافاصله یک استثنا ایجاد می کند. از این، دیباگر می‌تواند نتیجه بگیرد که inner() یک وعده رد شده را برمی‌گرداند، اما در حال حاضر چیزی در انتظار یا به‌طور دیگری آن وعده را مدیریت نمی‌کند. اشکال‌زدا می‌تواند حدس بزند که outer() احتمالاً منتظر آن خواهد بود و حدس می‌زند که این کار را در بلوک try فعلی‌اش انجام می‌دهد و بنابراین آن را مدیریت می‌کند، اما اشکال‌زدا نمی‌تواند تا زمانی که وعده رد شده برگردانده شود و در نهایت به عبارت await برسد، مطمئن باشد.

اشکال‌زدا نمی‌تواند تضمینی برای دقیق بودن پیش‌بینی‌های گرفتن ارائه دهد، اما از انواع اکتشافی برای الگوهای کدگذاری رایج برای پیش‌بینی صحیح استفاده می‌کند. برای درک این الگوها، به یادگیری نحوه کارکرد وعده ها کمک می کند.

در V8، یک Promise جاوا اسکریپت به عنوان یک شی نشان داده می شود که می تواند در یکی از سه حالت باشد: انجام شده، رد شده، یا در انتظار. اگر یک وعده در حالت انجام شده باشد و متد .then() را فراخوانی کنید، یک وعده در انتظار جدید ایجاد می‌شود و یک وظیفه واکنش وعده جدید برنامه‌ریزی می‌شود که کنترل کننده را اجرا می‌کند و سپس با نتیجه کنترل‌کننده، وعده را روی رد (rejected) تنظیم می‌کند. همین اتفاق می افتد اگر متد .catch() را روی یک وعده رد شده فراخوانی کنید. برعکس، فراخوانی .then() در یک وعده رد شده یا .catch() در یک وعده محقق شده، یک وعده را در همان حالت برمی گرداند و کنترل کننده را اجرا نمی کند.

یک وعده در انتظار شامل یک لیست واکنش است که در آن هر شی واکنش شامل یک کنترل کننده تحقق یا کنترل کننده رد (یا هر دو) و یک وعده واکنش است. بنابراین فراخوانی .then() روی یک وعده در انتظار، یک واکنش با یک کنترل کننده انجام شده و همچنین یک وعده در انتظار جدید برای وعده واکنش اضافه می کند، که .then() برمی گردد. فراخوانی .catch() یک واکنش مشابه اما با یک کنترل کننده رد اضافه می کند. فراخوانی .then() با دو آرگومان یک واکنش با هر دو هندلر ایجاد می کند و فراخوانی .finally() یا انتظار وعده، واکنشی با دو handler اضافه می کند که توابع داخلی مخصوص اجرای این ویژگی ها هستند.

وقتی وعده معلق در نهایت محقق شد یا رد شد، کارهای واکنش برای همه کنترل‌کننده‌های انجام‌شده یا همه کنترل‌کننده‌های رد شده برنامه‌ریزی می‌شود. سپس وعده‌های واکنش مربوطه به‌روزرسانی می‌شوند و به طور بالقوه مشاغل واکنش خود را آغاز می‌کنند.

نمونه ها

کد زیر را در نظر بگیرید:

return new Promise(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

ممکن است واضح نباشد که این کد شامل سه شیء Promise متمایز است. کد بالا معادل کد زیر می باشد:

const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;

در این مثال، مراحل زیر اتفاق می افتد:

  1. سازنده Promise نامیده می شود.
  2. یک Promise جدید در انتظار ایجاد شد.
  3. تابع ناشناس اجرا می شود.
  4. استثنا انداخته می شود. در این مرحله، دیباگر باید تصمیم بگیرد که متوقف شود یا نه.
  5. سازنده وعده این استثنا را می گیرد و سپس وضعیت وعده خود را به rejected با مقدار تنظیم شده آن به خطای پرتاب شده تغییر می دهد. این وعده را برمی گرداند که در promise1 ذخیره شده است.
  6. .then() هیچ عکس العملی را برنامه ریزی نمی کند زیرا promise1 در حالت rejected است. در عوض، یک وعده جدید ( promise2 ) برگردانده می شود که آن هم با همان خطا در حالت رد شده است.
  7. .catch() یک کار واکنش را با کنترل کننده ارائه شده و یک وعده واکنش معلق جدید را برنامه ریزی می کند که به عنوان promise3 برگردانده می شود. در این مرحله اشکال‌زدا می‌داند که خطا رسیدگی خواهد شد.
  8. هنگامی که وظیفه واکنش اجرا می شود، کنترل کننده به طور عادی برمی گردد و وضعیت promise3 به fulfilled تغییر می کند.

مثال بعدی ساختار مشابهی دارد اما اجرا کاملا متفاوت است:

return Promise.resolve()
    .then(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

این معادل است با:

const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;

در این مثال، مراحل زیر اتفاق می افتد:

  1. یک Promise در حالت fulfilled ایجاد می شود و در promise1 ذخیره می شود.
  2. یک وظیفه واکنش وعده با اولین تابع ناشناس برنامه ریزی می شود و وعده واکنش (pending) آن به عنوان promise2 برگردانده می شود.
  3. یک واکنش با یک کنترل کننده برآورده شده و وعده واکنش آن به promise2 اضافه می شود که به عنوان promise3 برگردانده می شود.
  4. یک واکنش با یک کنترل کننده رد شده به promise3 اضافه می شود و یک وعده واکنش دیگر که به عنوان promise4 برگردانده می شود.
  5. وظیفه واکنش برنامه ریزی شده در مرحله 2 اجرا می شود.
  6. کنترل کننده یک استثنا می اندازد. در این مرحله دیباگر باید تصمیم بگیرد که متوقف شود یا نه. در حال حاضر کنترل کننده تنها کد جاوا اسکریپت در حال اجرا شماست.
  7. از آنجایی که کار با یک استثنا به پایان می رسد، وعده واکنش مرتبط ( promise2 ) به حالت رد شده با مقدار آن روی خطای تنظیم شده تنظیم می شود.
  8. از آنجا که promise2 یک واکنش داشت و آن واکنش هیچ کنترل کننده رد شده ای نداشت، وعده واکنش آن ( promise3 ) نیز با همان خطا rejected می شود.
  9. از آنجا که promise3 یک واکنش داشت، و آن واکنش دارای یک کنترل کننده رد شده بود، یک وظیفه واکنش وعده با آن کنترل کننده و وعده واکنش آن برنامه ریزی می شود ( promise4 ).
  10. هنگامی که آن وظیفه واکنش اجرا می شود، کنترل کننده به طور عادی برمی گردد و حالت promise4 به تحقق یافته تغییر می کند.

روش های پیش بینی صید

دو منبع اطلاعاتی بالقوه برای پیش بینی صید وجود دارد. یکی پشته تماس است. این صدا برای استثناهای همزمان است: اشکال‌زدا می‌تواند پشته تماس را به همان روشی طی کند که کد بازگشایی استثنا انجام می‌شود و اگر فریمی را پیدا کند که در یک بلوک try...catch است، متوقف می‌شود. برای وعده‌ها یا استثناهای رد شده در سازنده‌های وعده یا در توابع ناهمزمان که هرگز تعلیق نشده‌اند، اشکال‌زدا نیز به پشته تماس متکی است، اما در این مورد، پیش‌بینی آن در همه موارد نمی‌تواند قابل اعتماد باشد. این به این دلیل است که به جای پرتاب یک استثنا به نزدیک‌ترین کنترل‌کننده، کد ناهمزمان یک استثنا رد شده را برمی‌گرداند و اشکال‌زدا باید چند فرض در مورد کاری که تماس‌گیرنده با آن انجام می‌دهد انجام دهد.

ابتدا، اشکال‌زدا فرض می‌کند که تابعی که یک وعده بازگشتی دریافت می‌کند احتمالاً آن وعده یا یک وعده مشتق شده را برمی‌گرداند، بنابراین توابع ناهمزمان بالاتر از پشته فرصتی برای انتظار آن را داشته باشند. دوم، اشکال‌زدا فرض می‌کند که اگر یک وعده به یک تابع ناهمزمان برگردانده شود، به زودی بدون ورود یا خروج از یک بلوک try...catch منتظر آن می‌ماند. هیچ یک از این فرضیات تضمین نمی شود که درست باشند، اما برای پیش بینی صحیح برای رایج ترین الگوهای کدگذاری با توابع ناهمزمان کافی هستند. در کروم نسخه 125، یک اکتشافی دیگر اضافه کردیم: اشکال‌زدا بررسی می‌کند که آیا فراخواننده می‌خواهد .catch() را روی مقداری که برگردانده می‌شود (یا .then() با دو آرگومان، یا زنجیره‌ای از فراخوانی‌ها به .then() یا .finally() و به دنبال آن یک .catch() یا یک .then() ) .t. در این حالت، اشکال‌زدا فرض می‌کند که این‌ها روش‌هایی هستند که روی قولی که در حال ردیابی آن هستیم یا یکی از روش‌های مرتبط با آن هستند، بنابراین رد شدن صورت می‌گیرد.

منبع دوم اطلاعات درخت واکنش های وعده است. دیباگر با یک وعده ریشه شروع می شود. گاهی اوقات این یک وعده است که متد reject() آن به تازگی فراخوانی شده است. معمولاً، هنگامی که یک استثنا یا رد در طول یک کار واکنش وعده اتفاق می‌افتد، و به نظر می‌رسد چیزی در پشته تماس آن را نمی‌گیرد، اشکال‌زدا از قول مرتبط با واکنش ردیابی می‌کند. اشکال‌زدا همه واکنش‌های مربوط به وعده‌های معلق را بررسی می‌کند و می‌بیند که آیا آنها کنترل‌کننده‌های رد دارند یا خیر. اگر هر واکنشی انجام نشود، به وعده واکنش نگاه می کند و به صورت بازگشتی از آن ردیابی می کند. اگر همه واکنش‌ها در نهایت منجر به یک کنترل کننده رد شوند، اشکال‌زدا رد وعده را محرز می‌کند. موارد خاصی برای پوشش وجود دارد، به عنوان مثال، عدم احتساب کنترل کننده رد داخلی برای فراخوانی .finally() finally.

درخت واکنش وعده منبع اطلاعاتی معمولاً قابل اعتمادی را در صورت وجود اطلاعات فراهم می کند. در برخی موارد، مانند فراخوانی به Promise.reject() یا در سازنده Promise یا در یک تابع async که هنوز منتظر چیزی نیست، هیچ واکنشی برای ردیابی وجود نخواهد داشت و اشکال‌زدا باید به پشته تماس تکیه کند. در موارد دیگر، درخت واکنش وعده معمولاً شامل کنترل‌کننده‌های لازم برای استنتاج پیش‌بینی گرفتن است، اما همیشه این امکان وجود دارد که بعداً کنترل‌کننده‌های بیشتری اضافه شوند که استثنا را از catch به uncack یا برعکس تغییر دهند. همچنین وعده‌هایی مانند وعده‌هایی وجود دارد که توسط Promise.all/any/race ایجاد شده است، که در آن سایر وعده‌های گروه ممکن است بر نحوه برخورد با رد تأثیر بگذارد. برای این روش‌ها، اشکال‌زدا فرض می‌کند که در صورتی که وعده هنوز معلق باشد، رد قول ارسال می‌شود.

به دو مثال زیر توجه کنید:

دو مثال برای پیش بینی صید

در حالی که این دو نمونه از استثناهای گرفته شده مشابه به نظر می رسند، اما به اکتشافی پیش بینی گرفتن کاملاً متفاوتی نیاز دارند. در مثال اول، یک وعده حل‌شده ایجاد می‌شود، سپس یک واکنش واکنش برای .then() برنامه‌ریزی می‌شود که یک استثنا ایجاد می‌کند، سپس .catch() فراخوانی می‌شود تا یک کنترل کننده رد را به وعده واکنش متصل کند. هنگامی که وظیفه واکنش اجرا می‌شود، استثنا پرتاب می‌شود و درخت واکنش وعده حاوی کنترل‌کننده catch است، بنابراین به‌عنوان catch شناسایی می‌شود. در مثال دوم، قول بلافاصله قبل از اجرای کد اضافه کردن یک کنترل کننده رد می شود، بنابراین هیچ کنترل کننده رد در درخت واکنش وعده وجود ندارد. اشکال‌زدا باید به پشته تماس نگاه کند، اما بلوک‌های try...catch نیز وجود ندارد. برای پیش‌بینی صحیح این موضوع، اشکال‌زدا قبل از مکان فعلی در کد، فراخوانی به .catch() را اسکن می‌کند و بر این اساس فرض می‌کند که رد در نهایت انجام می‌شود.

خلاصه

امیدواریم این توضیح نحوه کارکرد پیش‌بینی گرفتن در ابزار توسعه کروم، نقاط قوت و محدودیت‌های آن را روشن کرده باشد. اگر به دلیل پیش‌بینی‌های نادرست با مشکلات اشکال‌زدایی مواجه شدید، این گزینه‌ها را در نظر بگیرید:

  • الگوی کدگذاری را به چیزی ساده تر برای پیش بینی تغییر دهید، مانند استفاده از توابع همگام.
  • اگر DevTools در زمانی که باید متوقف نشود، برای شکستن همه استثناها انتخاب کنید.
  • اگر اشکال‌زدا در جایی که شما نمی‌خواهید متوقف می‌شود، از نقطه شکست «هرگز در اینجا مکث نکنید» یا نقطه شکست شرطی استفاده کنید.

قدردانی ها

عمیق ترین قدردانی ما از سوفیا املیانوا و جسلین ین برای کمک ارزشمند آنها در ویرایش این پست است!

،

اریک لیز
Eric Leese

اشکال زدایی استثناها در برنامه های کاربردی وب ساده به نظر می رسد: زمانی که مشکلی پیش می آید اجرای را متوقف کنید و بررسی کنید. اما ماهیت ناهمزمان جاوا اسکریپت این را به طرز شگفت آوری پیچیده می کند. چگونه می‌تواند Chrome DevTools بداند زمانی که استثناها از طریق وعده‌ها و توابع ناهمزمان عبور می‌کنند، چه زمانی و کجا مکث کند؟

این پست به چالش‌های پیش‌بینی شکار می‌پردازد – توانایی DevTools برای پیش‌بینی اینکه آیا یک استثنا بعداً در کد شما ثبت می‌شود یا خیر. ما بررسی خواهیم کرد که چرا اینقدر مشکل است و چگونه پیشرفت‌های اخیر در V8 (موتور جاوا اسکریپت که کروم را تامین می‌کند) آن را دقیق‌تر می‌کند و منجر به تجربه اشکال‌زدایی روان‌تر می‌شود.

چرا پیش‌بینی مهم است

در Chrome DevTools، گزینه‌ای دارید که اجرای کد را فقط برای استثناهای کشف نشده متوقف کنید، و از مواردی که دستگیر شده‌اند رد شوید.

Chrome DevTools گزینه‌های جداگانه‌ای را برای مکث در موارد استثنا شده یا کشف نشده ارائه می‌کند

در پشت صحنه، هنگامی که یک استثنا برای حفظ متن رخ می دهد، اشکال زدا بلافاصله متوقف می شود. این یک پیش‌بینی است، زیرا در حال حاضر نمی‌توان مطمئن شد که آیا استثنا بعداً در کد ثبت می‌شود یا نه، به خصوص در سناریوهای ناهمزمان. این عدم قطعیت از دشواری ذاتی پیش‌بینی رفتار برنامه، مشابه مشکل توقف ، ناشی می‌شود.

مثال زیر را در نظر بگیرید: کجا باید دیباگر مکث کند؟ (در بخش بعدی به دنبال پاسخ باشید.)

async function inner() {
  throw new Error(); // Should the debugger pause here?
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ... or should the debugger pause here?
  }
}

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

پیش بینی های نادرست منجر به ناامیدی می شود:

  • منفی های کاذب (پیش بینی "غیر گیر" زمانی که گرفتار می شود) . توقف های غیر ضروری در دیباگر.
  • موارد مثبت کاذب (پیش‌بینی "گرفتار" زمانی که کشف نشود) . فرصت‌های از دست رفته برای دریافت خطاهای مهم، به طور بالقوه شما را مجبور می‌کند همه استثناها، از جمله موارد مورد انتظار را اشکال‌زدایی کنید.

روش دیگر برای کاهش وقفه‌های اشکال‌زدایی، استفاده از فهرست نادیده گرفته می‌شود، که از شکستن استثناها در کد شخص ثالث مشخص شده جلوگیری می‌کند. با این حال، پیش‌بینی دقیق صید هنوز در اینجا بسیار مهم است. اگر استثنایی که از کد شخص ثالث منشأ می گیرد فرار کرد و بر روی کد شما تأثیر گذاشت، باید بتوانید آن را اشکال زدایی کنید.

کد ناهمزمان چگونه کار می کند

Promises، async و await و سایر الگوهای ناهمزمان می‌توانند به سناریوهایی منجر شوند که در آن یک استثنا یا رد، قبل از رسیدگی، ممکن است مسیر اجرایی را طی کند که در زمان ایجاد استثنا، تعیین آن دشوار است. این به این دلیل است که ممکن است منتظر وعده‌ها نباشند یا تا زمانی که استثنا رخ داده باشد، کنترل‌کننده‌های catch اضافه شوند. بیایید به مثال قبلی خود نگاه کنیم:

async function inner() {
  throw new Error();
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ...
  }
}

در این مثال، outer() ابتدا inner() را فراخوانی می کند که بلافاصله یک استثنا ایجاد می کند. از این، دیباگر می‌تواند نتیجه بگیرد که inner() یک وعده رد شده را برمی‌گرداند، اما در حال حاضر چیزی در انتظار یا به‌طور دیگری آن وعده را مدیریت نمی‌کند. اشکال‌زدا می‌تواند حدس بزند که outer() احتمالاً منتظر آن خواهد بود و حدس می‌زند که این کار را در بلوک try فعلی‌اش انجام می‌دهد و بنابراین آن را مدیریت می‌کند، اما اشکال‌زدا نمی‌تواند تا زمانی که وعده رد شده برگردانده شود و در نهایت به عبارت await برسد، مطمئن باشد.

اشکال‌زدا نمی‌تواند تضمینی برای دقیق بودن پیش‌بینی‌های گرفتن ارائه دهد، اما از انواع اکتشافی برای الگوهای کدگذاری رایج برای پیش‌بینی صحیح استفاده می‌کند. برای درک این الگوها، به یادگیری نحوه کارکرد وعده ها کمک می کند.

در V8، یک Promise جاوا اسکریپت به عنوان یک شی نشان داده می شود که می تواند در یکی از سه حالت باشد: انجام شده، رد شده، یا در انتظار. اگر یک وعده در حالت انجام شده باشد و متد .then() را فراخوانی کنید، یک وعده در انتظار جدید ایجاد می‌شود و یک وظیفه واکنش وعده جدید برنامه‌ریزی می‌شود که کنترل کننده را اجرا می‌کند و سپس با نتیجه کنترل‌کننده، وعده را روی رد (rejected) تنظیم می‌کند. همین اتفاق می افتد اگر متد .catch() را روی یک وعده رد شده فراخوانی کنید. برعکس، فراخوانی .then() در یک وعده رد شده یا .catch() در یک وعده محقق شده، یک وعده را در همان حالت برمی گرداند و کنترل کننده را اجرا نمی کند.

یک وعده در انتظار شامل یک لیست واکنش است که در آن هر شی واکنش شامل یک کنترل کننده تحقق یا کنترل کننده رد (یا هر دو) و یک وعده واکنش است. بنابراین فراخوانی .then() روی یک وعده در انتظار، یک واکنش با یک کنترل کننده انجام شده و همچنین یک وعده در انتظار جدید برای وعده واکنش اضافه می کند، که .then() برمی گردد. فراخوانی .catch() یک واکنش مشابه اما با یک کنترل کننده رد اضافه می کند. فراخوانی .then() با دو آرگومان یک واکنش با هر دو هندلر ایجاد می کند و فراخوانی .finally() یا انتظار وعده، واکنشی با دو handler اضافه می کند که توابع داخلی مخصوص اجرای این ویژگی ها هستند.

وقتی وعده معلق در نهایت محقق شد یا رد شد، کارهای واکنش برای همه کنترل‌کننده‌های انجام‌شده یا همه کنترل‌کننده‌های رد شده برنامه‌ریزی می‌شود. سپس وعده‌های واکنش مربوطه به‌روزرسانی می‌شوند و به طور بالقوه مشاغل واکنش خود را آغاز می‌کنند.

نمونه ها

کد زیر را در نظر بگیرید:

return new Promise(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

ممکن است واضح نباشد که این کد شامل سه شیء Promise متمایز است. کد بالا معادل کد زیر می باشد:

const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;

در این مثال، مراحل زیر اتفاق می افتد:

  1. سازنده Promise نامیده می شود.
  2. یک Promise جدید در انتظار ایجاد شد.
  3. تابع ناشناس اجرا می شود.
  4. استثنا انداخته می شود. در این مرحله، دیباگر باید تصمیم بگیرد که متوقف شود یا نه.
  5. سازنده وعده این استثنا را می گیرد و سپس وضعیت وعده خود را به rejected با مقدار تنظیم شده آن به خطای پرتاب شده تغییر می دهد. این وعده را برمی گرداند که در promise1 ذخیره شده است.
  6. .then() هیچ عکس العملی را برنامه ریزی نمی کند زیرا promise1 در حالت rejected است. در عوض، یک وعده جدید ( promise2 ) برگردانده می شود که آن هم با همان خطا در حالت رد شده است.
  7. .catch() یک کار واکنش را با کنترل کننده ارائه شده و یک وعده واکنش معلق جدید را برنامه ریزی می کند که به عنوان promise3 برگردانده می شود. در این مرحله اشکال‌زدا می‌داند که خطا رسیدگی خواهد شد.
  8. هنگامی که وظیفه واکنش اجرا می شود، کنترل کننده به طور عادی برمی گردد و وضعیت promise3 به fulfilled تغییر می کند.

مثال بعدی ساختار مشابهی دارد اما اجرا کاملا متفاوت است:

return Promise.resolve()
    .then(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

این معادل است با:

const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;

در این مثال، مراحل زیر اتفاق می افتد:

  1. یک Promise در حالت fulfilled ایجاد می شود و در promise1 ذخیره می شود.
  2. یک وظیفه واکنش وعده با اولین تابع ناشناس برنامه ریزی می شود و وعده واکنش (pending) آن به عنوان promise2 برگردانده می شود.
  3. یک واکنش با یک کنترل کننده برآورده شده و وعده واکنش آن به promise2 اضافه می شود که به عنوان promise3 برگردانده می شود.
  4. یک واکنش با یک کنترل کننده رد شده به promise3 اضافه می شود و یک وعده واکنش دیگر که به عنوان promise4 برگردانده می شود.
  5. وظیفه واکنش برنامه ریزی شده در مرحله 2 اجرا می شود.
  6. کنترل کننده یک استثنا می اندازد. در این مرحله دیباگر باید تصمیم بگیرد که متوقف شود یا نه. در حال حاضر کنترل کننده تنها کد جاوا اسکریپت در حال اجرا شماست.
  7. از آنجایی که کار با یک استثنا به پایان می رسد، وعده واکنش مرتبط ( promise2 ) به حالت رد شده با مقدار آن روی خطای تنظیم شده تنظیم می شود.
  8. از آنجا که promise2 یک واکنش داشت و آن واکنش هیچ کنترل کننده رد شده ای نداشت، وعده واکنش آن ( promise3 ) نیز با همان خطا rejected می شود.
  9. از آنجا که promise3 یک واکنش داشت، و آن واکنش دارای یک کنترل کننده رد شده بود، یک وظیفه واکنش وعده با آن کنترل کننده و وعده واکنش آن برنامه ریزی می شود ( promise4 ).
  10. هنگامی که آن وظیفه واکنش اجرا می شود، کنترل کننده به طور عادی برمی گردد و حالت promise4 به تحقق یافته تغییر می کند.

روش های پیش بینی صید

دو منبع اطلاعاتی بالقوه برای پیش بینی صید وجود دارد. یکی پشته تماس است. این صدا برای استثناهای همزمان است: اشکال‌زدا می‌تواند پشته تماس را به همان روشی طی کند که کد بازگشایی استثنا انجام می‌شود و اگر فریمی را پیدا کند که در یک بلوک try...catch است، متوقف می‌شود. برای وعده‌ها یا استثناهای رد شده در سازنده‌های وعده یا در توابع ناهمزمان که هرگز تعلیق نشده‌اند، اشکال‌زدا نیز به پشته تماس متکی است، اما در این مورد، پیش‌بینی آن در همه موارد نمی‌تواند قابل اعتماد باشد. این به این دلیل است که به جای پرتاب یک استثنا به نزدیک‌ترین کنترل‌کننده، کد ناهمزمان یک استثنا رد شده را برمی‌گرداند و اشکال‌زدا باید چند فرض در مورد کاری که تماس‌گیرنده با آن انجام می‌دهد انجام دهد.

ابتدا، اشکال‌زدا فرض می‌کند که تابعی که یک وعده بازگشتی دریافت می‌کند احتمالاً آن وعده یا یک وعده مشتق شده را برمی‌گرداند، بنابراین توابع ناهمزمان بالاتر از پشته فرصتی برای انتظار آن را داشته باشند. دوم، اشکال‌زدا فرض می‌کند که اگر یک وعده به یک تابع ناهمزمان برگردانده شود، به زودی بدون ورود یا خروج از یک بلوک try...catch منتظر آن می‌ماند. هیچ یک از این فرضیات تضمین نمی شود که درست باشند، اما برای پیش بینی صحیح برای رایج ترین الگوهای کدگذاری با توابع ناهمزمان کافی هستند. در کروم نسخه 125، یک اکتشافی دیگر اضافه کردیم: اشکال‌زدا بررسی می‌کند که آیا فراخواننده می‌خواهد .catch() را روی مقداری که برگردانده می‌شود (یا .then() با دو آرگومان، یا زنجیره‌ای از فراخوانی‌ها به .then() یا .finally() و به دنبال آن یک .catch() یا یک .then() ) .t. در این حالت، اشکال‌زدا فرض می‌کند که این‌ها روش‌هایی هستند که روی قولی که در حال ردیابی آن هستیم یا یکی از روش‌های مرتبط با آن هستند، بنابراین رد شدن صورت می‌گیرد.

منبع دوم اطلاعات درخت واکنش های وعده است. دیباگر با یک وعده ریشه شروع می شود. گاهی اوقات این یک وعده است که متد reject() آن به تازگی فراخوانی شده است. معمولاً، هنگامی که یک استثنا یا رد در طول یک کار واکنش وعده اتفاق می‌افتد، و به نظر می‌رسد چیزی در پشته تماس آن را نمی‌گیرد، اشکال‌زدا از قول مرتبط با واکنش ردیابی می‌کند. اشکال‌زدا همه واکنش‌های مربوط به وعده‌های معلق را بررسی می‌کند و می‌بیند که آیا آنها کنترل‌کننده‌های رد دارند یا خیر. اگر هر واکنشی انجام نشود، به وعده واکنش نگاه می کند و به صورت بازگشتی از آن ردیابی می کند. اگر همه واکنش‌ها در نهایت منجر به یک کنترل کننده رد شوند، اشکال‌زدا رد وعده را محرز می‌کند. موارد خاصی برای پوشش وجود دارد، به عنوان مثال، عدم احتساب کنترل کننده رد داخلی برای فراخوانی .finally() finally.

درخت واکنش وعده منبع اطلاعاتی معمولاً قابل اعتمادی را در صورت وجود اطلاعات فراهم می کند. در برخی موارد، مانند فراخوانی به Promise.reject() یا در سازنده Promise یا در یک تابع async که هنوز منتظر چیزی نیست، هیچ واکنشی برای ردیابی وجود نخواهد داشت و اشکال‌زدا باید به پشته تماس تکیه کند. در موارد دیگر، درخت واکنش وعده معمولاً شامل کنترل‌کننده‌های لازم برای استنتاج پیش‌بینی گرفتن است، اما همیشه این امکان وجود دارد که بعداً کنترل‌کننده‌های بیشتری اضافه شوند که استثنا را از catch به uncack یا برعکس تغییر دهند. همچنین وعده‌هایی مانند وعده‌هایی وجود دارد که توسط Promise.all/any/race ایجاد شده است، که در آن سایر وعده‌های گروه ممکن است بر نحوه برخورد با رد تأثیر بگذارد. برای این روش‌ها، اشکال‌زدا فرض می‌کند که در صورتی که وعده هنوز معلق باشد، رد قول ارسال می‌شود.

به دو مثال زیر توجه کنید:

دو مثال برای پیش بینی صید

در حالی که این دو نمونه از استثناهای گرفته شده مشابه به نظر می رسند، اما به اکتشافی پیش بینی گرفتن کاملاً متفاوتی نیاز دارند. در مثال اول، یک وعده حل‌شده ایجاد می‌شود، سپس یک واکنش واکنش برای .then() برنامه‌ریزی می‌شود که یک استثنا ایجاد می‌کند، سپس .catch() فراخوانی می‌شود تا یک کنترل کننده رد را به وعده واکنش متصل کند. هنگامی که وظیفه واکنش اجرا می‌شود، استثنا پرتاب می‌شود و درخت واکنش وعده حاوی کنترل‌کننده catch است، بنابراین به‌عنوان catch شناسایی می‌شود. در مثال دوم، قول بلافاصله قبل از اجرای کد اضافه کردن یک کنترل کننده رد می شود، بنابراین هیچ کنترل کننده رد در درخت واکنش وعده وجود ندارد. اشکال‌زدا باید به پشته تماس نگاه کند، اما بلوک‌های try...catch نیز وجود ندارد. برای پیش‌بینی صحیح این موضوع، اشکال‌زدا قبل از مکان فعلی در کد، فراخوانی به .catch() را اسکن می‌کند و بر این اساس فرض می‌کند که رد در نهایت انجام می‌شود.

خلاصه

امیدواریم این توضیح نحوه کارکرد پیش‌بینی گرفتن در ابزار توسعه کروم، نقاط قوت و محدودیت‌های آن را روشن کرده باشد. اگر به دلیل پیش‌بینی‌های نادرست با مشکلات اشکال‌زدایی مواجه شدید، این گزینه‌ها را در نظر بگیرید:

  • الگوی کدگذاری را به چیزی ساده تر برای پیش بینی تغییر دهید، مانند استفاده از توابع همگام.
  • اگر DevTools در زمانی که باید متوقف نشود، برای شکستن همه استثناها انتخاب کنید.
  • اگر اشکال‌زدا در جایی که شما نمی‌خواهید متوقف می‌شود، از نقطه شکست «هرگز در اینجا مکث نکنید» یا نقطه شکست شرطی استفاده کنید.

قدردانی ها

عمیق ترین قدردانی ما از سوفیا املیانوا و جسلین ین برای کمک ارزشمند آنها در ویرایش این پست است!

،

اریک لیز
Eric Leese

اشکال زدایی استثناها در برنامه های کاربردی وب ساده به نظر می رسد: زمانی که مشکلی پیش می آید اجرای را متوقف کنید و بررسی کنید. اما ماهیت ناهمزمان جاوا اسکریپت این را به طرز شگفت آوری پیچیده می کند. چگونه می‌تواند Chrome DevTools بداند زمانی که استثناها از طریق وعده‌ها و توابع ناهمزمان عبور می‌کنند، چه زمانی و کجا مکث کند؟

این پست به چالش‌های پیش‌بینی شکار می‌پردازد – توانایی DevTools برای پیش‌بینی اینکه آیا یک استثنا بعداً در کد شما ثبت می‌شود یا خیر. ما بررسی خواهیم کرد که چرا اینقدر مشکل است و چگونه پیشرفت‌های اخیر در V8 (موتور جاوا اسکریپت که کروم را تامین می‌کند) آن را دقیق‌تر می‌کند و منجر به تجربه اشکال‌زدایی روان‌تر می‌شود.

چرا پیش‌بینی مهم است

در Chrome DevTools، گزینه‌ای دارید که اجرای کد را فقط برای استثناهای کشف نشده متوقف کنید، و از مواردی که دستگیر شده‌اند رد شوید.

Chrome DevTools گزینه‌های جداگانه‌ای را برای مکث در موارد استثنا شده یا کشف نشده ارائه می‌کند

در پشت صحنه، هنگامی که یک استثنا برای حفظ متن رخ می دهد، اشکال زدا بلافاصله متوقف می شود. این یک پیش‌بینی است، زیرا در حال حاضر نمی‌توان مطمئن شد که آیا استثنا بعداً در کد ثبت می‌شود یا نه، به خصوص در سناریوهای ناهمزمان. این عدم قطعیت از دشواری ذاتی پیش‌بینی رفتار برنامه، مشابه مشکل توقف ، ناشی می‌شود.

مثال زیر را در نظر بگیرید: کجا باید دیباگر مکث کند؟ (در بخش بعدی به دنبال پاسخ باشید.)

async function inner() {
  throw new Error(); // Should the debugger pause here?
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ... or should the debugger pause here?
  }
}

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

پیش بینی های نادرست منجر به ناامیدی می شود:

  • منفی های کاذب (پیش بینی "غیر گیر" زمانی که گرفتار می شود) . توقف های غیر ضروری در دیباگر.
  • موارد مثبت کاذب (پیش‌بینی "گرفتار" زمانی که کشف نشود) . فرصت‌های از دست رفته برای دریافت خطاهای مهم، به طور بالقوه شما را مجبور می‌کند همه استثناها، از جمله موارد مورد انتظار را اشکال‌زدایی کنید.

روش دیگر برای کاهش وقفه‌های اشکال‌زدایی، استفاده از فهرست نادیده گرفته می‌شود، که از شکستن استثناها در کد شخص ثالث مشخص شده جلوگیری می‌کند. با این حال، پیش‌بینی دقیق صید هنوز در اینجا بسیار مهم است. اگر استثنایی که از کد شخص ثالث منشأ می گیرد فرار کرد و بر روی کد شما تأثیر گذاشت، باید بتوانید آن را اشکال زدایی کنید.

نحوه عملکرد کدهای ناهمزمان

Promises، async و await و سایر الگوهای ناهمزمان می‌توانند به سناریوهایی منجر شوند که در آن یک استثنا یا رد، قبل از رسیدگی، ممکن است مسیر اجرایی را طی کند که در زمان ایجاد استثنا، تعیین آن دشوار است. این به این دلیل است که ممکن است منتظر وعده‌ها نباشند یا تا زمانی که استثنا رخ داده باشد، کنترل‌کننده‌های catch اضافه شوند. بیایید به مثال قبلی خود نگاه کنیم:

async function inner() {
  throw new Error();
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ...
  }
}

در این مثال، outer() ابتدا inner() را فراخوانی می کند که بلافاصله یک استثنا ایجاد می کند. از این، دیباگر می‌تواند نتیجه بگیرد که inner() یک وعده رد شده را برمی‌گرداند، اما در حال حاضر چیزی در انتظار یا به‌طور دیگری آن وعده را مدیریت نمی‌کند. اشکال‌زدا می‌تواند حدس بزند که outer() احتمالاً منتظر آن خواهد بود و حدس می‌زند که این کار را در بلوک try فعلی‌اش انجام می‌دهد و بنابراین آن را مدیریت می‌کند، اما اشکال‌زدا نمی‌تواند تا زمانی که وعده رد شده برگردانده شود و در نهایت به عبارت await برسد، مطمئن باشد.

اشکال‌زدا نمی‌تواند تضمینی برای دقیق بودن پیش‌بینی‌های گرفتن ارائه دهد، اما از انواع اکتشافی برای الگوهای کدگذاری رایج برای پیش‌بینی صحیح استفاده می‌کند. برای درک این الگوها، به یادگیری نحوه کارکرد وعده ها کمک می کند.

در V8، یک Promise جاوا اسکریپت به عنوان یک شی نشان داده می شود که می تواند در یکی از سه حالت باشد: انجام شده، رد شده، یا در انتظار. اگر یک وعده در حالت انجام شده باشد و متد .then() را فراخوانی کنید، یک وعده در انتظار جدید ایجاد می‌شود و یک وظیفه واکنش وعده جدید برنامه‌ریزی می‌شود که کنترل کننده را اجرا می‌کند و سپس با نتیجه کنترل‌کننده، وعده را روی رد (rejected) تنظیم می‌کند. همین اتفاق می افتد اگر متد .catch() را روی یک وعده رد شده فراخوانی کنید. برعکس، فراخوانی .then() در یک وعده رد شده یا .catch() در یک وعده محقق شده، یک وعده را در همان حالت برمی گرداند و کنترل کننده را اجرا نمی کند.

یک وعده در انتظار شامل یک لیست واکنش است که در آن هر شی واکنش شامل یک کنترل کننده تحقق یا کنترل کننده رد (یا هر دو) و یک وعده واکنش است. بنابراین فراخوانی .then() روی یک وعده در انتظار، یک واکنش با یک کنترل کننده انجام شده و همچنین یک وعده در انتظار جدید برای وعده واکنش اضافه می کند، که .then() برمی گردد. فراخوانی .catch() یک واکنش مشابه اما با یک کنترل کننده رد اضافه می کند. فراخوانی .then() با دو آرگومان یک واکنش با هر دو هندلر ایجاد می کند و فراخوانی .finally() یا انتظار وعده، واکنشی با دو handler اضافه می کند که توابع داخلی مخصوص اجرای این ویژگی ها هستند.

وقتی وعده معلق در نهایت محقق شد یا رد شد، کارهای واکنش برای همه کنترل‌کننده‌های انجام‌شده یا همه کنترل‌کننده‌های رد شده برنامه‌ریزی می‌شود. سپس وعده‌های واکنش مربوطه به‌روزرسانی می‌شوند و به طور بالقوه مشاغل واکنش خود را آغاز می‌کنند.

نمونه ها

کد زیر را در نظر بگیرید:

return new Promise(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

ممکن است واضح نباشد که این کد شامل سه شیء Promise متمایز است. کد بالا معادل کد زیر می باشد:

const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;

در این مثال، مراحل زیر اتفاق می افتد:

  1. سازنده Promise نامیده می شود.
  2. یک Promise جدید در انتظار ایجاد شد.
  3. تابع ناشناس اجرا می شود.
  4. استثنا انداخته می شود. در این مرحله، دیباگر باید تصمیم بگیرد که متوقف شود یا نه.
  5. سازنده وعده این استثنا را می گیرد و سپس وضعیت وعده خود را به rejected با مقدار تنظیم شده آن به خطای پرتاب شده تغییر می دهد. این وعده را برمی گرداند که در promise1 ذخیره شده است.
  6. .then() هیچ عکس العملی را برنامه ریزی نمی کند زیرا promise1 در حالت rejected است. در عوض، یک وعده جدید ( promise2 ) برگردانده می شود که آن هم با همان خطا در حالت رد شده است.
  7. .catch() یک کار واکنش را با کنترل کننده ارائه شده و یک وعده واکنش معلق جدید را برنامه ریزی می کند که به عنوان promise3 برگردانده می شود. در این مرحله ، اشکال زدایی می داند که این خطا انجام خواهد شد.
  8. هنگامی که کار واکنش انجام می شود ، کنترل کننده به طور عادی برمی گردد و وضعیت promise3 برای fulfilled تغییر می کند.

مثال بعدی یک ساختار مشابه دارد اما اجرای آن کاملاً متفاوت است:

return Promise.resolve()
    .then(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

این معادل است با:

const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;

در این مثال ، مراحل زیر اتفاق می افتد:

  1. Promise ای در حالت fulfilled ایجاد می شود و در promise1 ذخیره می شود.
  2. یک کار واکنش وعده با اولین عملکرد ناشناس برنامه ریزی شده است و وعده واکنش آن (pending) آن به عنوان promise2 بازگردانده می شود.
  3. واکنشی به promise2 با یک کنترل کننده برآورده و وعده واکنش آن اضافه می شود ، که به عنوان promise3 بازگردانده می شود.
  4. واکنشی به promise3 با یک کنترل کننده رد شده و یک وعده واکنش دیگر اضافه می شود ، که به عنوان promise4 بازگردانده می شود.
  5. وظیفه واکنش برنامه ریزی شده در مرحله 2 اجرا شده است.
  6. کنترل کننده یک استثنا را پرتاب می کند. در این مرحله ، اشکال زدایی باید تصمیم بگیرد که متوقف شود یا نه. در حال حاضر کنترل کننده تنها کد JavaScript شما است.
  7. از آنجا که کار با استثنا به پایان می رسد ، وعده واکنش مرتبط ( promise2 ) با مقدار آن بر روی خطایی که پرتاب شده است ، روی حالت رد شده تنظیم می شود.
  8. از آنجا که promise2 یک واکنش داشت ، و این واکنش هیچ کنترل کننده رد شده ای نداشت ، وعده واکنش آن ( promise3 ) نیز با همان خطا rejected می شود.
  9. از آنجا که promise3 یک واکنش داشت ، و این واکنش دارای یک کنترل کننده رد شده بود ، یک کار واکنش وعده با آن کنترل کننده و وعده واکنش آن ( promise4 ) برنامه ریزی شده است.
  10. هنگامی که این کار واکنش انجام می شود ، کنترل کننده به طور عادی برمی گردد و وضعیت promise4 برای تحقق تغییر می کند.

روشهای پیش بینی گرفتن

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

اول ، اشکال زدایی فرض می کند که تابعی که یک قول برگشتی را دریافت می کند ، احتمالاً آن وعده یا یک وعده مشتق شده را بازگرداند تا عملکردهای ناهمزمان بیشتر از پشته فرصتی برای انتظار آن داشته باشند. دوم ، اشکال زدایی فرض می کند که اگر قول به یک عملکرد ناهمزمان بازگردانده شود ، به زودی بدون ورود یا ترک try...catch در انتظار آن خواهد بود. هیچ یک از این فرضیات صحیح نیست اما آنها برای پیش بینی های صحیح برای رایج ترین الگوهای برنامه نویسی با توابع ناهمزمان کافی هستند. در نسخه Chrome 125 ، ما یک اکتشافی دیگر اضافه کردیم: اشکال زدایی را بررسی می کند که آیا یک کاللی در حال تماس با .catch() .then() ارزش است که با دو استدلال بازگردانده می شود ، یا یک زنجیره تماس به .then() یا .finally() و به دنبال آن یک .catch() یا یک Argument .then() ). در این حالت ، اشکال زدایی فرض می کند که این روشهای وعده ای است که ما در حال ردیابی هستیم یا یکی از آنها در رابطه با آن است ، بنابراین رد این امر گرفتار می شود.

منبع دوم اطلاعات درخت واکنش های وعده است. اشکال زدایی با یک وعده ریشه شروع می شود. بعضی اوقات این وعده ای است که به تازگی روش reject() آن خوانده شده است. به طور معمول ، هنگامی که یک استثناء یا طرد در طی یک کار واکنش وعده اتفاق می افتد ، و به نظر نمی رسد که هیچ چیز در پشته تماس آن را بدست آورد ، اشکال زدایی از وعده مرتبط با واکنش ردیابی می کند. این اشکال زدایی به تمام واکنشهای مربوط به وعده در انتظار نگاه می کند و می بیند که آیا آنها دستگیرندگان رد دارند. اگر هرگونه واکنشی انجام ندهد ، به وعده واکنش نگاه می کند و به صورت بازگشتی از آن اثری می کند. اگر همه واکنش ها در نهایت منجر به یک کنترل کننده رد شود ، اشکال زدایی این وعده را گرفتار می کند. به عنوان مثال موارد خاصی برای پوشش دادن وجود دارد ، به عنوان مثال ، حساب کاربری رد داخلی را برای یک تماس .finally() شمارش نمی کند.

اگر اطلاعات در آنجا وجود داشته باشد ، درخت واکنش وعده دهنده منبع اطلاعاتی معمولاً قابل اعتماد است. در بعضی موارد ، مانند فراخوانی به Promise.reject() یا در یک سازنده Promise یا در یک عملکرد async که هنوز در انتظار چیزی نبوده است ، هیچ واکنشی برای ردیابی وجود نخواهد داشت و اشکال زدایی باید به تنهایی به پشته تماس اعتماد کند. در موارد دیگر ، درخت واکنش وعده معمولاً شامل دستگیره های لازم برای استنباط پیش بینی می شود ، اما همیشه ممکن است که بعداً تعداد بیشتری از دستگیرندگان اضافه شوند که این استثنا را از گرفتار به نامشخص یا برعکس تغییر می دهد. همچنین وعده هایی مانند آنهایی که توسط Promise.all/any/race ایجاد شده اند وجود دارد. برای این روشها ، اشکال زدایی فرض می کند که اگر قول هنوز در انتظار باشد ، رد قول ارسال می شود.

به دو مثال زیر توجه کنید:

دو نمونه برای پیش بینی گرفتن

در حالی که این دو نمونه از استثنائات گرفتار مشابه به نظر می رسد ، آنها نیاز به اکتشافی کاملاً متفاوت پیش بینی می کنند. در مثال اول ، یک وعده حل شده ایجاد می شود ، سپس یک کار واکنش برای .then() برنامه ریزی شده است که یک استثنا را به وجود می آورد ، سپس .catch() فراخوانده می شود تا یک کنترل کننده رد را به وعده واکنش متصل کند. هنگامی که کار واکنش انجام می شود ، استثناء پرتاب می شود و درخت واکنش وعده حاوی کنترل کننده صید خواهد بود ، بنابراین به عنوان گرفتار تشخیص داده می شود. در مثال دوم ، قول بلافاصله قبل از اجرای کد برای اضافه کردن یک کنترل کننده صید ، رد می شود ، بنابراین هیچ دستگیرنده رد در درخت واکنش وعده وجود ندارد. اشکال زدایی باید به پشته تماس نگاه کند اما هیچ try...catch . برای پیش بینی صحیح این موضوع ، اشکال زدایی پیش از مکان فعلی در کد برای یافتن تماس با .catch() اسکن می کند و بر این اساس فرض می کند که رد در نهایت انجام می شود.

خلاصه

امیدوارم این توضیح در مورد چگونگی عملکرد پیش بینی Catch در Devtools Chrome ، نقاط قوت و محدودیت های آن روشن شود. اگر به دلیل پیش بینی های نادرست با مشکلات اشکال زدایی روبرو شدید ، این گزینه ها را در نظر بگیرید:

  • برای پیش بینی ، مانند استفاده از توابع Async ، الگوی برنامه نویسی را به چیزی ساده تر تغییر دهید.
  • اگر DevTools نتواند در صورت لزوم متوقف شود ، همه استثنائات را انتخاب کنید.
  • اگر اشکال زدایی در جایی متوقف می شود که نمی خواهید آن را متوقف کنید ، از نقطه شکست "هرگز در اینجا مکث کنید" استفاده کنید.

قدردانی ها

عمیق ترین قدردانی ما از صوفیه املیانووا و جیکلین یین به دلیل کمک ارزشمند آنها برای ویرایش این پست بیرون می آید!

،

اریک لیز
Eric Leese

استثنائات اشکال زدایی در برنامه های وب ساده به نظر می رسد: اجرای مکث هنگامی که چیزی پیش می رود و تحقیق می کند. اما ماهیت ناهمزمان جاوا اسکریپت این مسئله را به طرز شگفت آور پیچیده می کند. چگونه می توان Devtools Chrome را دانست که چه موقع و از کجا مکث می کنند وقتی استثنائات از طریق وعده ها و توابع ناهمزمان پرواز می کنند؟

این پست به چالش های پیش بینی صید می پردازد - توانایی DevTools در پیش بینی اینکه آیا یک استثنا بعداً در کد شما گرفتار خواهد شد. ما بررسی خواهیم کرد که چرا اینقدر مشکل است و چگونه پیشرفت های اخیر در V8 (موتور JavaScript Powering Chrome) آن را دقیق تر می کند و منجر به یک تجربه اشکال زدایی نرم تر می شود.

چرا پیش بینی گرفتن مهم است

در Chrome Devtools ، شما گزینه ای برای مکث اجرای کد فقط برای استثنائات غیرقانونی ، پرش از مواردی که گرفتار شده اند ، دارید.

Chrome Devtools گزینه های جداگانه ای برای مکث در استثنائات گرفتار یا نامشخص ارائه می دهد

در پشت صحنه ، هنگامی که یک استثنا برای حفظ زمینه وجود دارد ، اشکال زدایی بلافاصله متوقف می شود. این یک پیش بینی است زیرا ، در این لحظه غیرممکن است که با اطمینان بدانید که آیا این استثناء بعداً در کد به ویژه در سناریوهای ناهمزمان گرفتار می شود یا نه. این عدم اطمینان ناشی از دشواری ذاتی پیش بینی رفتار برنامه ، مشابه مشکل متوقف است .

مثال زیر را در نظر بگیرید: اشکال زدایی کجا باید مکث کند؟ (در بخش بعدی به دنبال پاسخ باشید.)

async function inner() {
  throw new Error(); // Should the debugger pause here?
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ... or should the debugger pause here?
  }
}

مکث در مورد استثنائات در یک اشکال زدایی می تواند مختل کننده باشد و منجر به وقفه های مکرر و پرش به کد ناآشنا می شود. برای کاهش این موضوع ، می توانید فقط استثنائات اشکال زدایی را انتخاب کنید ، که به احتمال زیاد علائم واقعی را نشان می دهد. با این حال ، این به صحت پیش بینی صید متکی است.

پیش بینی های نادرست منجر به ناامیدی می شود:

  • منفی های کاذب (پیش بینی "نامشخص" چه زمانی گرفتار می شود) . توقف های غیر ضروری در اشکال زدایی.
  • مثبت کاذب (پیش بینی "گرفتار" چه موقع عدم آموزش) . فرصت های از دست رفته برای رسیدگی به خطاهای مهم ، به طور بالقوه شما را وادار به اشکال زدایی همه استثنائات ، از جمله موارد مورد انتظار می کند.

روش دیگر برای کاهش وقفه های اشکال زدایی ، استفاده از لیست نادیده گرفتن است که از شکستن استثنائات در کد شخص ثالث مشخص جلوگیری می کند. با این حال ، پیش بینی دقیق صید هنوز در اینجا بسیار مهم است. اگر یک استثناء که در کد شخص ثالث وجود دارد فرار می کند و بر کد شخصی شما تأثیر می گذارد ، می خواهید آن را اشکال زدایی کنید.

کد ناهمزمان چگونه کار می کند

وعده ها ، async و await ، و سایر الگوهای ناهمزمان می تواند منجر به سناریوهایی شود که در آن یک استثناء یا طرد ، قبل از رسیدگی ، ممکن است یک مسیر اعدام را طی کند که تعیین آن در زمان پرتاب یک استثنا دشوار است. این امر به این دلیل است که ممکن است وعده ها مورد انتظار نباشند یا دستگیرنده های صید اضافه شده تا بعد از اینکه استثناء قبلاً رخ داده است. بیایید به مثال قبلی خود نگاه کنیم:

async function inner() {
  throw new Error();
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ...
  }
}

در این مثال ، outer() ابتدا با inner() تماس می گیرد که بلافاصله یک استثنا را می کند. از این طریق اشکال زدایی می تواند نتیجه بگیرد که inner() قول رد شده ای را برمی گرداند ، اما در حال حاضر هیچ چیز در انتظار یا در غیر این صورت دست زدن به آن وعده نیست. اشکال زدایی می تواند حدس بزند که outer() احتمالاً در انتظار آن خواهد بود و حدس می زند که این کار را در بلوک try فعلی خود انجام خواهد داد و بنابراین آن را اداره می کند اما اشکال زدایی نمی تواند از این امر اطمینان داشته باشد تا اینکه پس از بازگشت قول رد شده و در نهایت بیانیه await حاصل می شود.

اشکال زدایی نمی تواند هیچ تضمینی را ارائه دهد که پیش بینی های صید دقیق خواهد بود اما از انواع اکتشافی برای الگوهای کدگذاری مشترک برای پیش بینی صحیح استفاده می کند. برای درک این الگوهای ، به یادگیری نحوه عملکرد وعده ها کمک می کند.

در V8 ، یک Promise جاوا اسکریپت به عنوان شیئی که می تواند در یکی از سه ایالت باشد ، ارائه می شود: برآورده ، رد یا در انتظار. اگر یک وعده در حالت تحقق یافته باشد و شما آن را روش .then() بنامید ، یک وعده جدید در انتظار ایجاد می شود و یک کار واکنش وعده جدید برنامه ریزی شده است که کنترل کننده را اجرا می کند و سپس وعده تحقق با نتیجه کنترل کننده را تنظیم می کند یا اگر کنترل کننده یک استثنا را انجام دهد ، رد می شود. در صورت فراخوانی روش .catch() به قول رد شده نیز همین اتفاق می افتد. برعکس ، فراخوانی .then() بر روی یک وعده رد شده یا .catch() بر روی یک وعده برآورده شده ، یک وعده را در همان حالت بازگرداند و کنترل کننده را اجرا نکنید.

یک وعده در حال تعلیق شامل یک لیست واکنش است که در آن هر شیء واکنش حاوی یک کنترل کننده برآورده یا کنترل کننده رد (یا هر دو) و یک وعده واکنش است. بنابراین فراخوانی .then() پس از آن در حال تعلیق ، واکنشی را با یک کنترل کننده برآورده و همچنین یک وعده جدید در انتظار وعده واکنش ، اضافه می کند ، که این .then() باز خواهد گشت. تماس با .catch() یک واکنش مشابه اما با یک کنترل کننده رد می افزاید. فراخوانی .then() با دو استدلال ، واکنشی را با هر دو دستگیرنده ایجاد می کند ، و در حال حاضر فراخوان .finally()

هنگامی که وعده در انتظار در نهایت برآورده یا رد می شود ، مشاغل واکنش برای همه دستگیرندگان برآورده شده یا همه دستگیرندگان رد شده آن برنامه ریزی می شوند. سپس وعده های واکنش مربوطه به روز می شوند ، و به طور بالقوه باعث ایجاد شغل واکنش خود می شوند.

نمونه ها

کد زیر را در نظر بگیرید:

return new Promise(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

شاید بدیهی نباشد که این کد شامل سه شیء Promise مجزا است. کد فوق معادل کد زیر است:

const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;

در این مثال ، مراحل زیر اتفاق می افتد:

  1. سازنده Promise خوانده می شود.
  2. یک Promise جدید در انتظار ایجاد شده است.
  3. عملکرد ناشناس اجرا می شود.
  4. یک استثنا پرتاب می شود. در این مرحله ، اشکال زدایی باید تصمیم بگیرد که متوقف شود یا نه.
  5. سازنده وعده این استثنا را به خود جلب می کند و سپس وضعیت وعده خود را برای rejected با ارزش خود به خطایی که پرتاب شده است تغییر می دهد. این قول را که در promise1 ذخیره می شود ، برمی گرداند.
  6. .then() هیچ برنامه واکنشی را برنامه ریزی نمی کند زیرا promise1 در حالت rejected است. در عوض ، یک وعده جدید ( promise2 ) بازگردانده می شود ، که با همان خطا نیز در حالت رد شده قرار دارد.
  7. .catch() یک کار واکنش را با کنترل کننده ارائه شده و یک وعده واکنش جدید در انتظار برنامه ریزی می کند ، که به عنوان promise3 بازگردانده می شود. در این مرحله ، اشکال زدایی می داند که این خطا انجام خواهد شد.
  8. هنگامی که کار واکنش انجام می شود ، کنترل کننده به طور عادی برمی گردد و وضعیت promise3 برای fulfilled تغییر می کند.

مثال بعدی یک ساختار مشابه دارد اما اجرای آن کاملاً متفاوت است:

return Promise.resolve()
    .then(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

این معادل است با:

const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;

در این مثال ، مراحل زیر اتفاق می افتد:

  1. Promise ای در حالت fulfilled ایجاد می شود و در promise1 ذخیره می شود.
  2. یک کار واکنش وعده با اولین عملکرد ناشناس برنامه ریزی شده است و وعده واکنش آن (pending) آن به عنوان promise2 بازگردانده می شود.
  3. واکنشی به promise2 با یک کنترل کننده برآورده و وعده واکنش آن اضافه می شود ، که به عنوان promise3 بازگردانده می شود.
  4. واکنشی به promise3 با یک کنترل کننده رد شده و یک وعده واکنش دیگر اضافه می شود ، که به عنوان promise4 بازگردانده می شود.
  5. وظیفه واکنش برنامه ریزی شده در مرحله 2 اجرا شده است.
  6. کنترل کننده یک استثنا را پرتاب می کند. در این مرحله ، اشکال زدایی باید تصمیم بگیرد که متوقف شود یا نه. در حال حاضر کنترل کننده تنها کد JavaScript شما است.
  7. از آنجا که کار با استثنا به پایان می رسد ، وعده واکنش مرتبط ( promise2 ) با مقدار آن بر روی خطایی که پرتاب شده است ، روی حالت رد شده تنظیم می شود.
  8. از آنجا که promise2 یک واکنش داشت ، و این واکنش هیچ کنترل کننده رد شده ای نداشت ، وعده واکنش آن ( promise3 ) نیز با همان خطا rejected می شود.
  9. از آنجا که promise3 یک واکنش داشت ، و این واکنش دارای یک کنترل کننده رد شده بود ، یک کار واکنش وعده با آن کنترل کننده و وعده واکنش آن ( promise4 ) برنامه ریزی شده است.
  10. هنگامی که این کار واکنش انجام می شود ، کنترل کننده به طور عادی برمی گردد و وضعیت promise4 برای تحقق تغییر می کند.

روشهای پیش بینی گرفتن

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

اول ، اشکال زدایی فرض می کند که تابعی که یک قول برگشتی را دریافت می کند ، احتمالاً آن وعده یا یک وعده مشتق شده را بازگرداند تا عملکردهای ناهمزمان بیشتر از پشته فرصتی برای انتظار آن داشته باشند. دوم ، اشکال زدایی فرض می کند که اگر قول به یک عملکرد ناهمزمان بازگردانده شود ، به زودی بدون ورود یا ترک try...catch در انتظار آن خواهد بود. هیچ یک از این فرضیات صحیح نیست اما آنها برای پیش بینی های صحیح برای رایج ترین الگوهای برنامه نویسی با توابع ناهمزمان کافی هستند. در نسخه Chrome 125 ، ما یک اکتشافی دیگر اضافه کردیم: اشکال زدایی را بررسی می کند که آیا یک کاللی در حال تماس با .catch() .then() ارزش است که با دو استدلال بازگردانده می شود ، یا یک زنجیره تماس به .then() یا .finally() و به دنبال آن یک .catch() یا یک Argument .then() ). در این حالت ، اشکال زدایی فرض می کند که این روشهای وعده ای است که ما در حال ردیابی هستیم یا یکی از آنها در رابطه با آن است ، بنابراین رد این امر گرفتار می شود.

منبع دوم اطلاعات درخت واکنش های وعده است. اشکال زدایی با یک وعده ریشه شروع می شود. بعضی اوقات این وعده ای است که به تازگی روش reject() آن خوانده شده است. به طور معمول ، هنگامی که یک استثناء یا طرد در طی یک کار واکنش وعده اتفاق می افتد ، و به نظر نمی رسد که هیچ چیز در پشته تماس آن را بدست آورد ، اشکال زدایی از وعده مرتبط با واکنش ردیابی می کند. این اشکال زدایی به تمام واکنشهای مربوط به وعده در انتظار نگاه می کند و می بیند که آیا آنها دستگیرندگان رد دارند. اگر هرگونه واکنشی انجام ندهد ، به وعده واکنش نگاه می کند و به صورت بازگشتی از آن اثری می کند. اگر همه واکنش ها در نهایت منجر به یک کنترل کننده رد شود ، اشکال زدایی این وعده را گرفتار می کند. به عنوان مثال موارد خاصی برای پوشش دادن وجود دارد ، به عنوان مثال ، حساب کاربری رد داخلی را برای یک تماس .finally() شمارش نمی کند.

اگر اطلاعات در آنجا وجود داشته باشد ، درخت واکنش وعده دهنده منبع اطلاعاتی معمولاً قابل اعتماد است. در بعضی موارد ، مانند فراخوانی به Promise.reject() یا در یک سازنده Promise یا در یک عملکرد async که هنوز در انتظار چیزی نبوده است ، هیچ واکنشی برای ردیابی وجود نخواهد داشت و اشکال زدایی باید به تنهایی به پشته تماس اعتماد کند. در موارد دیگر ، درخت واکنش وعده معمولاً شامل دستگیره های لازم برای استنباط پیش بینی می شود ، اما همیشه ممکن است که بعداً تعداد بیشتری از دستگیرندگان اضافه شوند که این استثنا را از گرفتار به نامشخص یا برعکس تغییر می دهد. همچنین وعده هایی مانند آنهایی که توسط Promise.all/any/race ایجاد شده اند وجود دارد. برای این روشها ، اشکال زدایی فرض می کند که اگر قول هنوز در انتظار باشد ، رد قول ارسال می شود.

به دو مثال زیر توجه کنید:

دو نمونه برای پیش بینی گرفتن

در حالی که این دو نمونه از استثنائات گرفتار مشابه به نظر می رسد ، آنها نیاز به اکتشافی کاملاً متفاوت پیش بینی می کنند. در مثال اول ، یک وعده حل شده ایجاد می شود ، سپس یک کار واکنش برای .then() برنامه ریزی شده است که یک استثنا را به وجود می آورد ، سپس .catch() فراخوانده می شود تا یک کنترل کننده رد را به وعده واکنش متصل کند. هنگامی که کار واکنش انجام می شود ، استثناء پرتاب می شود و درخت واکنش وعده حاوی کنترل کننده صید خواهد بود ، بنابراین به عنوان گرفتار تشخیص داده می شود. در مثال دوم ، قول بلافاصله قبل از اجرای کد برای اضافه کردن یک کنترل کننده صید ، رد می شود ، بنابراین هیچ دستگیرنده رد در درخت واکنش وعده وجود ندارد. اشکال زدایی باید به پشته تماس نگاه کند اما هیچ try...catch . برای پیش بینی صحیح این موضوع ، اشکال زدایی پیش از مکان فعلی در کد برای یافتن تماس با .catch() اسکن می کند و بر این اساس فرض می کند که رد در نهایت انجام می شود.

خلاصه

امیدوارم این توضیح در مورد چگونگی عملکرد پیش بینی Catch در Devtools Chrome ، نقاط قوت و محدودیت های آن روشن شود. اگر به دلیل پیش بینی های نادرست با مشکلات اشکال زدایی روبرو شدید ، این گزینه ها را در نظر بگیرید:

  • برای پیش بینی ، مانند استفاده از توابع Async ، الگوی برنامه نویسی را به چیزی ساده تر تغییر دهید.
  • اگر DevTools نتواند در صورت لزوم متوقف شود ، همه استثنائات را انتخاب کنید.
  • اگر اشکال زدایی در جایی متوقف می شود که نمی خواهید آن را متوقف کنید ، از نقطه شکست "هرگز در اینجا مکث کنید" استفاده کنید.

قدردانی ها

عمیق ترین قدردانی ما از صوفیه املیانووا و جیکلین یین به دلیل کمک ارزشمند آنها برای ویرایش این پست بیرون می آید!