Chrome DevTools में सुझाव देखना: यह मुश्किल क्यों है और इसे बेहतर कैसे बनाया जा सकता है

Eric Leese
Eric Leese

वेब ऐप्लिकेशन में अपवादों को डीबग करना आसान लगता है: कोई गड़बड़ी होने पर, प्रोग्राम को रोकें और जांच करें. हालांकि, JavaScript के एसिंक्रोनस होने की वजह से, यह काफ़ी मुश्किल हो जाता है. Chrome DevTools को कैसे पता चलता है कि प्रोमिस और असाइनॉन्स फ़ंक्शन के दौरान, अपवाद कब और कहां रोकने हैं?

इस पोस्ट में, अपवाद का अनुमान लगाने से जुड़ी चुनौतियों के बारे में बताया गया है. DevTools की यह सुविधा, आपके कोड में बाद में कोई अपवाद पकड़ा जाएगा या नहीं, इसका अनुमान लगाती है. हम जानेंगे कि यह इतना मुश्किल क्यों है और V8 (Chrome को चलाने वाला JavaScript इंजन) में हाल ही में किए गए सुधारों से, इसे ज़्यादा सटीक कैसे बनाया जा रहा है. इससे डीबग करने का अनुभव बेहतर हो रहा है.

कन्वर्ज़न का अनुमान लगाना क्यों ज़रूरी है 

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 के साथ-साथ, असाइनिमेंट के साथ काम करने वाले अन्य पैटर्न की वजह से, ऐसे मामले हो सकते हैं जिनमें किसी अपवाद या अस्वीकार किए जाने की स्थिति को मैनेज करने से पहले, उसे एक ऐसा पाथ मिल सकता है जिसे अपवाद मिलने के समय तय करना मुश्किल हो. ऐसा इसलिए है, क्योंकि अपवाद होने तक, प्रॉमिस का इंतज़ार नहीं किया जा सकता या उसमें कैच हैंडलर नहीं जोड़े जा सकते. आइए, पिछले उदाहरण को देखें:

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

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

इस उदाहरण में, outer() पहले inner() को कॉल करता है, जो तुरंत एक अपवाद दिखाता है. इससे डीबगर यह निष्कर्ष निकाल सकता है कि inner(), अस्वीकार किए गए प्रॉमिस को दिखाएगा. हालांकि, फ़िलहाल उस प्रॉमिस का इंतज़ार नहीं किया जा रहा है या उसे किसी दूसरे तरीके से मैनेज नहीं किया जा रहा है. डीबगर यह अनुमान लगा सकता है कि outer() शायद इसकी प्रतीक्षा करेगा और यह अनुमान लगा सकता है कि वह अपने मौजूदा try ब्लॉक में ऐसा करेगा और इसलिए उसे मैनेज करेगा. हालांकि, डीबगर इस बात की पुष्टि तब तक नहीं कर सकता, जब तक अस्वीकार किए गए प्रॉमिस को वापस नहीं लाया जाता और await स्टेटमेंट पर नहीं पहुंचा जाता.

डीबगर यह गारंटी नहीं दे सकता कि गड़बड़ी का अनुमान सटीक होगा. हालांकि, यह आम तौर पर इस्तेमाल होने वाले कोडिंग पैटर्न के लिए, अलग-अलग तरह के हेयुरिस्टिक्स का इस्तेमाल करके, सही अनुमान लगाता है. इन पैटर्न को समझने के लिए, यह जानना ज़रूरी है कि प्रॉमिस कैसे काम करते हैं.

V8 में, JavaScript 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. Promise कन्स्ट्रक्टर इस अपवाद को पकड़ता है और फिर अपने Promise की स्थिति को 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. दूसरे चरण में शेड्यूल किया गया प्रतिक्रिया टास्क चलाया जाता है.
  6. हैंडलर कोई अपवाद दिखाता है. इस स्थिति में, डीबगर को यह तय करना होगा कि प्रोग्राम को रोकना है या नहीं. फ़िलहाल, हैंडलर ही आपका एकमात्र चल रहा JavaScript कोड है.
  7. टास्क, अपवाद के साथ खत्म होने की वजह से, उससे जुड़े रिएक्शन प्रॉमिस (promise2) को अस्वीकार की गई स्थिति पर सेट किया जाता है. साथ ही, उसकी वैल्यू को उस गड़बड़ी पर सेट किया जाता है जो दिखी थी.
  8. promise2 पर एक प्रतिक्रिया दी गई थी और उस प्रतिक्रिया के लिए कोई भी हैंडलर अस्वीकार नहीं किया गया था. इसलिए, प्रतिक्रिया के प्रॉमिस (promise3) को भी उसी गड़बड़ी के साथ rejected पर सेट किया गया है.
  9. promise3 पर एक प्रतिक्रिया दी गई थी और उस प्रतिक्रिया के लिए, अस्वीकार किया गया हैंडलर था. इसलिए, उस हैंडलर और प्रतिक्रिया के प्रॉमिस (promise4) के साथ, प्रतिक्रिया देने का टास्क शेड्यूल किया गया है.
  10. प्रतिक्रिया देने का वह टास्क पूरा होने पर, हैंडलर सामान्य रूप से काम करता है और promise4 की स्थिति 'पूरा हो गया' में बदल जाती है.

वीडियो में ऑब्जेक्ट के पकड़े जाने का अनुमान लगाने के तरीके

मछली पकड़ने के अनुमान के लिए, जानकारी के दो संभावित सोर्स हैं. पहला, कॉल स्टैक. यह सिंक्रोनस अपवादों के लिए सही है: डीबगर, कॉल स्टैक को उसी तरह से वॉक कर सकता है जिस तरह अपवाद को अनवाइंड करने वाला कोड करता है. साथ ही, अगर उसे कोई ऐसा फ़्रेम मिलता है जहां वह try...catch ब्लॉक में है, तो वह रुक जाता है. डिबगर, कॉल स्टैक पर भी निर्भर करता है. ऐसा तब होता है, जब प्रोमिस कंस्ट्रक्टर या कभी भी निलंबित नहीं किए गए असाइनोक्रोनस फ़ंक्शन में, प्रोमिस अस्वीकार किए जाते हैं या अपवाद होते हैं. हालांकि, इस मामले में, सभी मामलों में इसके अनुमान पर भरोसा नहीं किया जा सकता. ऐसा इसलिए होता है, क्योंकि एसिंक्रोनस कोड, नज़दीकी हैंडलर को कोई अपवाद नहीं दिखाता. इसके बजाय, वह अस्वीकार किया गया अपवाद दिखाता है. साथ ही, डीबगर को कुछ अनुमान लगाने होते हैं कि कॉलर इसके साथ क्या करेगा.

सबसे पहले, डीबगर यह मानता है कि जिस फ़ंक्शन को रिटर्न किया गया प्रॉमिस मिलता है वह उस प्रॉमिस या उससे मिले प्रॉमिस को रिटर्न कर सकता है, ताकि स्टैक में आगे मौजूद असाइनोक्रोनस फ़ंक्शन को उसका इंतज़ार करने का मौका मिले. दूसरा, डीबगर यह मानता है कि अगर किसी असाइनिटिव फ़ंक्शन में कोई प्रॉमिस रिटर्न किया जाता है, तो वह try...catch ब्लॉक में प्रवेश किए बिना या उससे बाहर निकले बिना, जल्द ही उसका इंतज़ार करेगा. यह ज़रूरी नहीं है कि इनमें से कोई भी अनुमान सही हो, लेकिन एसिंक्रोनस फ़ंक्शन वाले सबसे सामान्य कोडिंग पैटर्न के लिए, सही अनुमान लगाने के लिए ये काफ़ी हैं. Chrome के वर्शन 125 में, हमने एक और अनुमानित तरीका जोड़ा है: डीबगर यह जांच करता है कि क्या कॉल पाने वाला, लौटाई जाने वाली वैल्यू पर .catch() को कॉल करने वाला है (या दो आर्ग्युमेंट के साथ .then() या .then() या .finally() को कॉल करने की चेन के बाद .catch() या दो आर्ग्युमेंट वाला .then()). इस मामले में, डीबगर यह मानता है कि ये ट्रैक किए जा रहे प्रॉमिस या उससे जुड़े तरीके हैं, ताकि अस्वीकार किए जाने की जानकारी मिल सके.

जानकारी का दूसरा सोर्स, प्रतिक्रियाओं का ट्री है. डीबगर, रूट प्रॉमिस से शुरू होता है. कभी-कभी यह ऐसा प्रॉमिस होता है जिसके reject() तरीके को अभी-अभी कॉल किया गया है. आम तौर पर, जब प्रतिक्रिया देने के लिए किए गए किसी वादे के दौरान कोई अपवाद या अस्वीकार होना होता है और कॉल स्टैक में कोई भी चीज़ उसे पकड़ नहीं पाती, तो डीबगर, प्रतिक्रिया से जुड़े वादे से ट्रैक करता है. डीबगर, पूरे नहीं हुए वादे पर की गई सभी प्रतिक्रियाओं को देखता है और यह पता लगाता है कि उनमें अस्वीकार करने वाले हैंडलर हैं या नहीं. अगर कोई प्रतिक्रिया नहीं मिलती है, तो यह प्रतिक्रिया के वादे को देखता है और उससे बार-बार ट्रैक करता है. अगर सभी प्रतिक्रियाओं के बाद, रिजेक्शन हैंडलर को ट्रिगर किया जाता है, तो डीबगर यह मान लेता है कि वादा पूरा नहीं हुआ. कुछ खास मामले हैं जिन्हें कवर करना ज़रूरी है. उदाहरण के लिए, .finally() कॉल के लिए, रिजेक्शन हैंडलर को शामिल न करना.

अगर जानकारी मौजूद है, तो आम तौर पर प्रतिक्रिया ट्री, जानकारी का भरोसेमंद सोर्स होता है. कुछ मामलों में, जैसे कि Promise.reject() को कॉल करने पर या Promise कन्स्ट्रक्टर में या किसी ऐसे असाइनिक फ़ंक्शन में जिसने अब तक किसी चीज़ का इंतज़ार नहीं किया है, ट्रैक करने के लिए कोई प्रतिक्रिया नहीं होगी. साथ ही, डीबगर को सिर्फ़ कॉल स्टैक पर भरोसा करना होगा. अन्य मामलों में, आम तौर पर प्रॉमिस रिऐक्शन ट्री में, कैच किए जाने के अनुमान का अनुमान लगाने के लिए ज़रूरी हैंडल होते हैं. हालांकि, यह हमेशा मुमकिन है कि बाद में ज़्यादा हैंडल जोड़े जाएं, जिससे अपवाद को पकड़े जाने से अनकच किए जाने में बदल दिया जाएगा या इसके उलट. Promise.all/any/race के बनाए गए वादे जैसे वादे भी होते हैं. इनमें ग्रुप के अन्य वादों का असर, अस्वीकार किए जाने के तरीके पर पड़ सकता है. इन तरीकों के लिए, डीबगर यह मानता है कि अगर वादा पूरा नहीं हुआ है, तो वादा अस्वीकार होने की सूचना भेजी जाएगी.

यहां दिए गए दो उदाहरण देखें:

मछली पकड़ने के अनुमान के दो उदाहरण

पकड़े गए अपवादों के ये दो उदाहरण एक जैसे दिखते हैं, लेकिन इनके लिए, अनुमान लगाने के अलग-अलग तरीके अपनाने पड़ते हैं. पहले उदाहरण में, रिज़ॉल्व की गई प्रॉमिस बनाई जाती है. इसके बाद, .then() के लिए प्रतिक्रिया वाली एक जॉब शेड्यूल की जाती है, जो अपवाद दिखाएगी. इसके बाद, प्रतिक्रिया वाली प्रॉमिस में अस्वीकार करने वाले हैंडलर को अटैच करने के लिए, .catch() को कॉल किया जाता है. प्रतिक्रिया देने वाला टास्क चलने पर, अपवाद को थ्रो किया जाएगा. साथ ही, प्रतिक्रिया देने वाले प्रॉमिस ट्री में, कैच हैंडलर शामिल होगा, ताकि उसे पकड़े जाने के तौर पर पहचाना जा सके. दूसरे उदाहरण में, कैच हैंडलर जोड़ने के लिए कोड चलने से पहले ही प्रॉमिस को अस्वीकार कर दिया जाता है. इसलिए, प्रॉमिस के रिऐक्शन ट्री में अस्वीकार करने वाले हैंडलर नहीं होते. डीबगर को कॉल स्टैक देखना चाहिए, लेकिन इसमें कोई try...catch ब्लॉक भी नहीं है. इस बारे में सही अनुमान लगाने के लिए, डीबगर कोड में मौजूदा जगह से आगे की ओर स्कैन करता है, ताकि .catch() को कॉल किया जा सके. साथ ही, इस आधार पर यह अनुमान लगाया जाता है कि अस्वीकार करने की कार्रवाई को आखिर में मैनेज कर लिया जाएगा.

खास जानकारी

उम्मीद है कि इस जानकारी से आपको यह समझने में मदद मिली होगी कि Chrome DevTools में, गड़बड़ी का अनुमान लगाने की सुविधा कैसे काम करती है. साथ ही, इसकी खूबियों और सीमाओं के बारे में भी पता चला होगा. अगर गलत अनुमान की वजह से, आपको डीबग करने में समस्याएं आ रही हैं, तो ये विकल्प आज़माएं:

  • कोडिंग पैटर्न को ऐसे बदलें जिससे अनुमान लगाना आसान हो. जैसे, एसिंक्रोनस फ़ंक्शन का इस्तेमाल करना.
  • अगर DevTools, तय समय पर रुक नहीं रहा है, तो सभी अपवादों पर ब्रेक लगाने का विकल्प चुनें.
  • अगर डीबगर किसी ऐसी जगह पर रुक रहा है जहां आपको उसे नहीं रोकना है, तो "यहां कभी भी रोकें" ब्रेकपॉइंट या स्थिति के हिसाब से ब्रेकपॉइंट का इस्तेमाल करें.

आभार

इस पोस्ट में बदलाव करने में हमारी मदद करने के लिए, सोफ़िया एमिलियानोवा और जेसलीन येन का बहुत-बहुत धन्यवाद!