JavaScript Promises: परिचय

प्रॉमिस की मदद से, बाद में होने वाली और एसिंक्रोनस कंप्यूटेशन को आसान बनाया जा सकता है. प्रॉमिस, ऐसी कार्रवाई को दिखाता है जो अभी तक पूरी नहीं हुई है.

डेवलपर, वेब डेवलपमेंट के इतिहास में एक अहम पल के लिए तैयार हो जाएं.

[ड्रमरोल शुरू होता है]

JavaScript में प्रॉमिस उपलब्ध हैं!

[आतिशबाज़ी होती है, ऊपर से चमकीले कागज़ गिरते हैं, भीड़ में उत्साह का माहौल है]

इस समय, आपको इनमें से किसी एक कैटगरी में रखा जाता है:

  • आपके आस-पास के लोग खुशी मना रहे हैं, लेकिन आपको नहीं पता कि ऐसा क्यों हो रहा है. शायद आपको यह भी नहीं पता कि "वादा" क्या होता है. आप कंधे उचकाते हैं, लेकिन ग्लिटर वाले कागज़ का वज़न आपके कंधों पर है. अगर ऐसा है, तो चिंता न करें. मुझे यह समझने में बहुत समय लगा कि मुझे इन चीज़ों की परवाह क्यों करनी चाहिए. आपको शायद शुरुआत से शुरू करना चाहिए.
  • आपने हवा में मुक्का मारा! अब समय आ गया है, है न? आपने इन प्रॉमिस का इस्तेमाल पहले भी किया है, लेकिन आपको इस बात से परेशानी है कि सभी इंप्लीमेंटेशन में थोड़ा अलग एपीआई है. JavaScript के आधिकारिक वर्शन के लिए एपीआई क्या है? शायद आपको शब्दावली से शुरुआत करनी चाहिए.
  • आपको इस बारे में पहले से पता है और आपको उन लोगों पर हंसी आ रही है जो इस खबर को सुनकर हैरान हैं. थोड़ा समय निकालकर, अपनी श्रेष्ठता का आनंद लें. इसके बाद, सीधे एपीआई रेफ़रंस पर जाएं.

ब्राउज़र के लिए सहायता और पॉलीफ़िल

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

जिन ब्राउज़र में प्रॉमिस को पूरी तरह से लागू नहीं किया गया है उन्हें खास निर्देशों के मुताबिक बनाने के लिए या अन्य ब्राउज़र और Node.js में प्रॉमिस जोड़ने के लिए, पॉलीफ़िल (2 केबी कंप्रेस किया गया) देखें.

यह सब क्या हो रहा है?

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

एक इंसान के तौर पर, आपके पास एक साथ कई काम करने की क्षमता होती है. एक साथ कई उंगलियों से टाइप किया जा सकता है. साथ ही, गाड़ी चलाते समय बातचीत भी की जा सकती है. हमें सिर्फ़ छींक आने की समस्या को ठीक करना है. छींक आने के दौरान, सभी मौजूदा गतिविधियों को रोकना होगा. यह बहुत परेशान करने वाला होता है. खास तौर पर, जब ड्राइविंग के दौरान बातचीत करनी हो. आपको ऐसा कोड नहीं लिखना चाहिए जो छींकने वाला हो.

आपने शायद इस समस्या को हल करने के लिए, इवेंट और कॉलबैक का इस्तेमाल किया हो. यहां इवेंट दिए गए हैं:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

इससे छींक नहीं आती. हमें इमेज मिलती है, हम कुछ लिसनर जोड़ते हैं, फिर JavaScript तब तक काम करना बंद कर सकती है, जब तक कि इनमें से किसी एक लिसनर को कॉल न किया जाए.

माफ़ करें, ऊपर दिए गए उदाहरण में ऐसा हो सकता है कि इवेंट, इमेज लोड होने से पहले ही हो गए हों. इसलिए, हमें इमेज की "complete" प्रॉपर्टी का इस्तेमाल करके, इस समस्या को ठीक करना होगा:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

इससे उन इमेज का पता नहीं चलता जिनमें गड़बड़ी हुई है. ऐसा इसलिए, क्योंकि हमें उनके बारे में सूचना नहीं मिली. अफ़सोस की बात है कि डीओएम हमें ऐसा करने का कोई तरीका नहीं देता. साथ ही, यह एक इमेज लोड कर रहा है. अगर हमें यह जानना हो कि इमेज का कोई सेट कब लोड हुआ है, तो चीज़ें और भी जटिल हो जाती हैं.

इवेंट हमेशा सबसे अच्छा तरीका नहीं होते

इवेंट उन चीज़ों के लिए बहुत अच्छे होते हैं जो एक ही ऑब्जेक्ट पर कई बार हो सकती हैं—keyup, touchstart वगैरह. इन इवेंट के साथ, आपको इस बात से कोई फ़र्क़ नहीं पड़ता कि लिसनर को अटैच करने से पहले क्या हुआ था. हालांकि, एसिंक प्रोसेस के पूरा होने या न होने की जानकारी के लिए, आपको इस तरह की जानकारी चाहिए:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

प्रॉमिस भी यही काम करते हैं, लेकिन इनके नाम बेहतर होते हैं. अगर एचटीएमएल इमेज एलिमेंट में "ready" नाम का कोई ऐसा तरीका होता जो प्रॉमिस दिखाता है, तो हम यह काम कर सकते थे:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

प्रॉमिस, इवेंट लिसनर की तरह होते हैं. हालांकि, इनमें ये अंतर होते हैं:

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

यह एसिंक ऑपरेशन के पूरा होने या फ़ेल होने की जानकारी देने के लिए बहुत काम का है. ऐसा इसलिए, क्योंकि आपको इस बात से ज़्यादा मतलब नहीं होता कि कोई चीज़ कब उपलब्ध हुई. आपको इस बात से ज़्यादा मतलब होता है कि नतीजे पर कैसे प्रतिक्रिया दी जाए.

प्रॉमिस की शब्दावली

डोमेनिक डेनिकोला ने इस लेख के पहले ड्राफ़्ट को पढ़ा और शब्दावली के लिए मुझे "F" ग्रेड दिया. उन्होंने मुझे स्कूल में रोक लिया. साथ ही, मुझसे स्टेट्स ऐंड फ़ेट्स को 100 बार कॉपी करने के लिए कहा. इसके अलावा, उन्होंने मेरे माता-पिता को एक पत्र भी लिखा, जिसमें उन्होंने मेरी पढ़ाई को लेकर चिंता ज़ाहिर की. इसके बावजूद, मुझे अब भी कई शब्दों के मतलब समझने में परेशानी होती है. हालांकि, यहां कुछ बुनियादी बातें दी गई हैं:

वादा इनमें से कोई एक हो सकता है:

  • fulfilled - वादे से जुड़ी कार्रवाई पूरी हो गई है
  • अस्वीकार किया गया - वादे से जुड़ी कार्रवाई पूरी नहीं हो सकी
  • मंज़ूरी बाकी है - अनुरोध को अब तक पूरा नहीं किया गया है या अस्वीकार नहीं किया गया है
  • settled - Has fulfilled or rejected

स्पेसिफ़िकेशन में, thenable शब्द का इस्तेमाल ऐसे ऑब्जेक्ट के लिए भी किया जाता है जो प्रॉमिस जैसा होता है. ऐसा इसलिए, क्योंकि इसमें then तरीका होता है. इस शब्द से मुझे इंग्लैंड के पूर्व फ़ुटबॉल मैनेजर टेरी वेनेबल्स की याद आती है. इसलिए, मैं इसका इस्तेमाल कम से कम करूंगा.

JavaScript में प्रॉमिस का इस्तेमाल किया जा सकता है!

प्रॉमिस, लाइब्रेरी के तौर पर कुछ समय से उपलब्ध हैं. जैसे:

ऊपर दिए गए और JavaScript प्रॉमिस में एक जैसा स्टैंडर्ड व्यवहार होता है. इसे Promises/A+ कहा जाता है. अगर आप jQuery का इस्तेमाल करते हैं, तो इसमें Deferreds नाम की मिलती-जुलती सुविधा होती है. हालांकि, Deferreds, Promise/A+ के मुताबिक नहीं होते. इसलिए, ये थोड़े अलग और कम काम के होते हैं. इसलिए, इनका इस्तेमाल करते समय सावधानी बरतें. jQuery में भी एक Promise टाइप होता है. हालांकि, यह Deferred का सिर्फ़ एक सबसेट है और इसमें भी वही समस्याएं होती हैं.

हालांकि, प्रॉमिस को लागू करने के तरीके में एक जैसा स्टैंडर्ड फ़ॉलो किया जाता है, लेकिन इनके एपीआई अलग-अलग होते हैं. JavaScript प्रॉमिस, RSVP.js के एपीआई में मिलते-जुलते हैं. यहां प्रॉमिस बनाने का तरीका बताया गया है:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

प्रॉमिस कंस्ट्रक्टर एक आर्ग्युमेंट लेता है. यह दो पैरामीटर वाला कॉलबैक होता है: resolve और reject. कॉलबैक के अंदर कोई कार्रवाई करें. जैसे, एसिंक कार्रवाई. इसके बाद, अगर सब कुछ ठीक से काम करता है, तो resolve को कॉल करें. ऐसा न होने पर, reject को कॉल करें.

सादी पुरानी JavaScript में throw की तरह, Error ऑब्जेक्ट के साथ अस्वीकार करना सामान्य है, लेकिन यह ज़रूरी नहीं है. गड़बड़ी वाले ऑब्जेक्ट का फ़ायदा यह है कि वे स्टैक ट्रेस को कैप्चर करते हैं. इससे डीबग करने वाले टूल ज़्यादा मददगार साबित होते हैं.

यहां बताया गया है कि इस वादे का इस्तेमाल कैसे किया जाता है:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() दो आर्ग्युमेंट लेता है. पहला, कॉल बैक के लिए और दूसरा, गड़बड़ी होने पर. दोनों ही ज़रूरी नहीं हैं. इसलिए, सिर्फ़ सफलता या गड़बड़ी के मामले में कॉलबैक जोड़ा जा सकता है.

JavaScript प्रॉमिस की शुरुआत DOM में "Futures" के तौर पर हुई थी. बाद में, इसका नाम बदलकर "Promises" कर दिया गया. आखिर में, इसे JavaScript में शामिल कर लिया गया. इन्हें DOM के बजाय JavaScript में रखने से फ़ायदा होता है, क्योंकि ये Node.js जैसे नॉन-ब्राउज़र JS कॉन्टेक्स्ट में उपलब्ध होंगे. हालांकि, ये अपने मुख्य एपीआई में इनका इस्तेमाल करते हैं या नहीं, यह एक अलग सवाल है.

हालांकि, ये JavaScript की सुविधा हैं, लेकिन DOM इनका इस्तेमाल करने से नहीं डरता. दरअसल, एसिंक तरीके से काम करने वाले सभी नए DOM API, प्रॉमिस का इस्तेमाल करेंगे. कोटा मैनेजमेंट, फ़ॉन्ट लोड इवेंट, ServiceWorker, Web MIDI, Streams वगैरह के साथ ऐसा पहले से हो रहा है.

अन्य लाइब्रेरी के साथ काम करने की क्षमता

JavaScript Promises API, then() तरीके वाली किसी भी चीज़ को प्रॉमिस की तरह (या प्रॉमिस की भाषा में thenable sigh) मानता है. इसलिए, अगर Q प्रॉमिस देने वाली किसी लाइब्रेरी का इस्तेमाल किया जाता है, तो कोई समस्या नहीं है. यह नई JavaScript Promises के साथ काम करेगी.

हालांकि, जैसा कि मैंने बताया, jQuery के Deferreds थोड़े … काम के नहीं हैं. हालांकि, इन्हें स्टैंडर्ड प्रॉमिस में बदला जा सकता है. इसलिए, इन्हें जल्द से जल्द बदलें:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

यहां, jQuery का $.ajax एक Deferred ऑब्जेक्ट दिखाता है. इसमें then() तरीका है. इसलिए, Promise.resolve() इसे JavaScript प्रॉमिस में बदल सकता है. हालांकि, कभी-कभी डीफ़र्ड, अपने कॉलबैक को कई आर्ग्युमेंट पास करते हैं. उदाहरण के लिए:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

वहीं, JS प्रॉमिस में पहले को छोड़कर बाकी सभी को अनदेखा किया जाता है:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

अच्छी बात यह है कि आम तौर पर, आपको वही डेटा मिलता है जो आपको चाहिए होता है. अगर ऐसा नहीं होता है, तो कम से कम आपको उस डेटा का ऐक्सेस मिल जाता है. यह भी ध्यान रखें कि jQuery, Error ऑब्जेक्ट को रिजेक्शन में पास करने के नियम का पालन नहीं करता है.

जटिल एसिंक्रोनस कोड को आसान बनाना

ठीक है, चलो कुछ कोड लिखते हैं. मान लें कि हमें:

  1. लोडिंग दिखाने के लिए स्पिनर शुरू करें
  2. किसी कहानी के लिए कुछ JSON फ़ेच करें. इससे हमें टाइटल और हर चैप्टर के यूआरएल मिलते हैं
  3. पेज में टाइटल जोड़ना
  4. हर चैप्टर को फ़ेच करना
  5. स्टोरी को पेज में जोड़ना
  6. स्पिनर को बंद करना

… लेकिन अगर कोई गड़बड़ी होती है, तो उपयोगकर्ता को इसकी जानकारी भी दें. हमें उस पॉइंट पर स्पिनर को भी रोकना होगा. ऐसा न करने पर, वह घूमता रहेगा, चक्कर खाएगा, और किसी दूसरे यूज़र इंटरफ़ेस (यूआई) में क्रैश हो जाएगा.

ज़ाहिर है, कहानी दिखाने के लिए JavaScript का इस्तेमाल नहीं किया जाएगा, क्योंकि एचटीएमएल के तौर पर कहानी दिखाने में कम समय लगता है. हालांकि, एपीआई के साथ काम करते समय यह पैटर्न काफ़ी सामान्य है: कई डेटा फ़ेच किए जाते हैं. इसके बाद, जब सभी डेटा फ़ेच हो जाते हैं, तब कुछ कार्रवाई की जाती है.

सबसे पहले, नेटवर्क से डेटा फ़ेच करने के बारे में जानते हैं:

XMLHttpRequest को प्रॉमिस में बदलना

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

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

अब इसका इस्तेमाल करते हैं:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

अब हम XMLHttpRequest को मैन्युअल तरीके से टाइप किए बिना एचटीटीपी अनुरोध कर सकते हैं. यह बहुत अच्छी बात है, क्योंकि XMLHttpRequest की ऊंट के कूबड़ जैसी स्टाइल को जितना कम देखना पड़ेगा, मेरी ज़िंदगी उतनी ही खुशहाल होगी.

चेनिंग

then() का इस्तेमाल सिर्फ़ यहीं तक सीमित नहीं है. वैल्यू में बदलाव करने या एक के बाद एक अतिरिक्त एसिंक कार्रवाइयां करने के लिए, then को एक साथ जोड़ा जा सकता है.

वैल्यू में बदलाव करना

नई वैल्यू को वापस लाकर, वैल्यू को आसानी से बदला जा सकता है:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

उदाहरण के तौर पर, आइए हम इस पर वापस जाते हैं:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

रिस्पॉन्स JSON फ़ॉर्मैट में है, लेकिन फ़िलहाल हमें यह सादे टेक्स्ट के तौर पर मिल रहा है. हम JSON responseType का इस्तेमाल करने के लिए, अपने get फ़ंक्शन में बदलाव कर सकते हैं. हालांकि, हम इसे प्रॉमिस लैंड में भी हल कर सकते हैं:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

JSON.parse() फ़ंक्शन में सिर्फ़ एक आर्ग्युमेंट होता है और यह बदली गई वैल्यू दिखाता है. इसलिए, हम एक शॉर्टकट बना सकते हैं:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

असल में, हम getJSON() फ़ंक्शन को बहुत आसानी से बना सकते हैं:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() अब भी एक प्रॉमिस दिखाता है. यह एक ऐसा प्रॉमिस है जो यूआरएल फ़ेच करता है और फिर रिस्पॉन्स को JSON के तौर पर पार्स करता है.

एसिंक्रोनस कार्रवाइयों को क्रम में लगाना

एसिंक्रोनस कार्रवाइयों को क्रम से चलाने के लिए, then को भी एक साथ जोड़ा जा सकता है.

then() कॉलबैक से कोई वैल्यू वापस करने पर, यह थोड़ा मुश्किल हो जाता है. अगर कोई वैल्यू दिखाई जाती है, तो अगली then() को उस वैल्यू के साथ कॉल किया जाता है. हालांकि, अगर आपने प्रॉमिस जैसा कुछ भी वापस किया है, तो अगला then() इसके पूरा होने का इंतज़ार करता है. साथ ही, इसे सिर्फ़ तब कॉल किया जाता है, जब वह प्रॉमिस पूरा हो जाता है (सफल/असफल). उदाहरण के लिए:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

यहां हम story.json को एसिंक्रोनस अनुरोध करते हैं. इससे हमें अनुरोध करने के लिए यूआरएल का एक सेट मिलता है. इसके बाद, हम उनमें से पहले यूआरएल का अनुरोध करते हैं. इस समय, प्रॉमिस, सामान्य कॉलबैक पैटर्न से अलग दिखते हैं.

चैप्टर पाने के लिए, शॉर्टकट तरीका भी बनाया जा सकता है:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

हम story.json को तब तक डाउनलोड नहीं करते, जब तक getChapter को कॉल नहीं किया जाता. हालांकि, अगली बार getChapter को कॉल किए जाने पर, हम स्टोरी प्रॉमिस का फिर से इस्तेमाल करते हैं. इसलिए, story.json को सिर्फ़ एक बार फ़ेच किया जाता है. Yay Promises!

गड़बड़ी ठीक करना

जैसा कि हमने पहले देखा, then() दो आर्ग्युमेंट लेता है. एक आर्ग्युमेंट, कार्रवाई के पूरा होने पर और दूसरा, कार्रवाई के पूरा न होने पर (या प्रॉमिस के हिसाब से, पूरा होने और अस्वीकार होने पर):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

catch() का इस्तेमाल भी किया जा सकता है:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() में कुछ खास नहीं है. यह सिर्फ़ then(undefined, func) के लिए एक तरह का शुगर है, लेकिन इसे पढ़ना आसान है. ध्यान दें कि ऊपर दिए गए दोनों कोड के उदाहरण एक जैसे नहीं हैं. दूसरा उदाहरण, इसके बराबर है:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

इनमें थोड़ा फ़र्क़ है, लेकिन ये बहुत काम के हैं. वादे पूरे न होने पर, अगले then() पर तुरंत रीडायरेक्ट कर दिया जाता है. साथ ही, अस्वीकार करने के लिए कॉलबैक (या catch(), क्योंकि यह इसके बराबर है) का इस्तेमाल किया जाता है. then(func1, func2) के साथ, func1 या func2 को कॉल किया जाएगा, दोनों को कभी नहीं. हालांकि, then(func1).catch(func2) के साथ, func1 के अस्वीकार होने पर दोनों को कॉल किया जाएगा, क्योंकि ये चेन में अलग-अलग चरण हैं. इनकी जानकारी दें:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

ऊपर दिया गया फ़्लो, सामान्य JavaScript try/catch से काफ़ी मिलता-जुलता है. "try" में होने वाली गड़बड़ियां तुरंत catch() ब्लॉक में चली जाती हैं. ऊपर दी गई जानकारी को फ़्लोचार्ट के तौर पर यहां दिखाया गया है (क्योंकि मुझे फ़्लोचार्ट पसंद हैं):

वादे पूरे करने के लिए नीली लाइनों का पालन करें या वादे अस्वीकार करने के लिए लाल लाइनों का पालन करें.

JavaScript अपवाद और प्रॉमिस

प्रॉमिस को साफ़ तौर पर अस्वीकार किए जाने पर, उसे अस्वीकार किया जाता है. हालांकि, अगर कंस्ट्रक्टर कॉलबैक में कोई गड़बड़ी होती है, तो उसे परोक्ष रूप से भी अस्वीकार किया जाता है:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

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

then() कॉलबैक में दिखने वाली गड़बड़ियों पर भी यही नियम लागू होता है.

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

प्रैक्टिस में गड़बड़ी ठीक करना

स्टोरी और चैप्टर की मदद से, हम उपयोगकर्ता को गड़बड़ी दिखाने के लिए कैच का इस्तेमाल कर सकते हैं:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

अगर story.chapterUrls[0] फ़ेच नहीं किया जा सका (जैसे, http 500 या उपयोगकर्ता ऑफ़लाइन है), तो यह सभी फ़ॉलोइंग सक्सेस कॉलबैक को स्किप कर देगा.इनमें getJSON() में मौजूद वह कॉलबैक भी शामिल है जो रिस्पॉन्स को JSON के तौर पर पार्स करने की कोशिश करता है. साथ ही, यह उस कॉलबैक को भी स्किप कर देता है जो पेज में chapter1.html जोड़ता है. इसके बजाय, यह कैच कॉलबैक पर चला जाता है. इस वजह से, अगर पिछली कोई कार्रवाई पूरी नहीं होती है, तो पेज पर "चैप्टर नहीं दिखाया जा सका" मैसेज दिखेगा.

JavaScript के try/catch की तरह, गड़बड़ी का पता लगाया जाता है और इसके बाद का कोड जारी रहता है. इसलिए, स्पिनर हमेशा छिपा रहता है. हमें यही चाहिए. ऊपर दिया गया कोड, इस कोड का नॉन-ब्लॉकिंग एसिंक वर्शन बन जाता है:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

आपको सिर्फ़ लॉगिंग के लिए catch() की ज़रूरत हो सकती है. ऐसा हो सकता है कि आपको गड़बड़ी ठीक न करनी हो. इसके लिए, गड़बड़ी को फिर से थ्रो करें. हम इसे getJSON() तरीके से कर सकते हैं:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

इसलिए, हम एक चैप्टर फ़ेच कर पाए हैं, लेकिन हमें सभी चैप्टर चाहिए. चलिए, अब आपके टारगेट को पूरा किया जाए.

पैरललिज़्म और सीक्वेंसिंग: दोनों का सबसे ज़्यादा फ़ायदा पाना

एसिंक के बारे में सोचना आसान नहीं है. अगर आपको कोड लिखने में समस्या आ रही है, तो कोड को इस तरह लिखें कि वह सिंक्रोनस हो. इस मामले में:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

यह काम करता है! हालांकि, इससे ब्राउज़र सिंक हो जाता है और डाउनलोड होने के दौरान लॉक हो जाता है. इस फ़ंक्शन को एसिंक बनाने के लिए, हम then() का इस्तेमाल करते हैं, ताकि एक के बाद एक काम हो सके.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

हालांकि, हम चैप्टर के यूआरएल को कैसे लूप कर सकते हैं और उन्हें क्रम से कैसे फ़ेच कर सकते हैं? यह काम नहीं करता:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach एसिंक-अवेयर नहीं है. इसलिए, हमारे चैप्टर उसी क्रम में दिखेंगे जिस क्रम में वे डाउनलोड होते हैं. Pulp Fiction को भी इसी तरह लिखा गया था. यह पल्प फ़िक्शन नहीं है, इसलिए इसे ठीक करते हैं.

कोई क्रम बनाना

हमें अपने chapterUrls अरे को प्रॉमिस के क्रम में बदलना है. then() का इस्तेमाल करके, हम ऐसा कर सकते हैं:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

हमने पहली बार Promise.resolve() देखा है. यह एक प्रॉमिस बनाता है, जो आपकी दी गई वैल्यू के हिसाब से रिज़ॉल्व होता है. अगर आपने इसे Promise का कोई इंस्टेंस दिया है, तो यह उसे वापस कर देगा (ध्यान दें: यह स्पेसिफ़िकेशन में किया गया बदलाव है, जिसका पालन अब तक कुछ लागू करने वाले लोग नहीं कर रहे हैं). अगर इसे प्रॉमिस जैसा कोई ऑब्जेक्ट (जिसमें then() तरीका हो) पास किया जाता है, तो यह एक असली Promise ऑब्जेक्ट बनाता है. यह ऑब्जेक्ट, प्रॉमिस की तरह ही पूरा होता है या खारिज होता है. अगर आपने कोई दूसरी वैल्यू पास की है, जैसे कि Promise.resolve('Hello'), यह एक प्रॉमिस बनाता है, जो उस वैल्यू के साथ पूरा होता है. अगर इसे बिना किसी वैल्यू के कॉल किया जाता है, तो ऊपर दिए गए उदाहरण की तरह, यह "undefined" के साथ पूरा होता है.

इसके अलावा, Promise.reject(val) भी है. यह एक ऐसा प्रॉमिस बनाता है जो दी गई वैल्यू (या अपरिभाषित) के साथ अस्वीकार कर देता है.

array.reduce का इस्तेमाल करके, ऊपर दिए गए कोड को व्यवस्थित किया जा सकता है:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

यह पिछले उदाहरण की तरह ही काम करता है, लेकिन इसके लिए अलग "sequence" वैरिएबल की ज़रूरत नहीं होती. रिड्यूस कॉलबैक को कैटगरी के हर आइटम के लिए कॉल किया जाता है. "sequence" की वैल्यू Promise.resolve() पहली बार में सेट की जाती है. इसके बाद, "sequence" की वैल्यू वही होती है जो पिछले कॉल में सेट की गई थी. array.reduce किसी ऐरे को एक वैल्यू में बदलने के लिए बहुत काम का होता है. इस मामले में, यह एक प्रॉमिस है.

आइए, इन सभी को एक साथ इस्तेमाल करें:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

यहां हमने सिंक वर्शन का पूरी तरह से एसिंक वर्शन तैयार कर लिया है. हालांकि, हम इसे और बेहतर बना सकते हैं. फ़िलहाल, हमारा पेज इस तरह डाउनलोड हो रहा है:

ब्राउज़र एक साथ कई चीज़ें डाउनलोड कर सकते हैं. इसलिए, एक के बाद एक चैप्टर डाउनलोड करने से परफ़ॉर्मेंस कम हो जाती है. हमें इन सभी फ़ाइलों को एक साथ डाउनलोड करना है. इसके बाद, जब ये सभी फ़ाइलें डाउनलोड हो जाएं, तब इन्हें प्रोसेस करना है. अच्छी बात यह है कि इसके लिए एक एपीआई उपलब्ध है:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all कई प्रॉमिस लेता है और एक ऐसा प्रॉमिस बनाता है जो तब पूरा होता है, जब वे सभी प्रॉमिस पूरे हो जाते हैं. आपको कई नतीजे मिलते हैं. ये नतीजे, उसी क्रम में होते हैं जिस क्रम में आपने वादे पास किए थे.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

कनेक्शन के हिसाब से, एक-एक करके लोड करने की तुलना में, यह कुछ सेकंड तेज़ी से लोड हो सकता है. साथ ही, यह हमारे पहले तरीके से कम कोड है. चैप्टर किसी भी क्रम में डाउनलोड हो सकते हैं. हालांकि, वे स्क्रीन पर सही क्रम में दिखते हैं.

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

इसके लिए, हम अपने सभी चैप्टर के लिए एक साथ JSON फ़ेच करते हैं. इसके बाद, उन्हें दस्तावेज़ में जोड़ने के लिए एक क्रम बनाते हैं:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

और यहां दोनों का सबसे अच्छा वर्शन है! सभी कॉन्टेंट को डिलीवर करने में उतना ही समय लगता है, लेकिन उपयोगकर्ता को कॉन्टेंट का पहला हिस्सा पहले मिल जाता है.

इस सामान्य उदाहरण में, सभी चैप्टर एक ही समय पर दिख रहे हैं. हालांकि, ज़्यादा और बड़े चैप्टर होने पर, एक-एक करके चैप्टर दिखाने का फ़ायदा ज़्यादा होगा.

Node.js-स्टाइल वाले कॉलबैक या इवेंट का इस्तेमाल करके ऊपर दिया गया काम करने के लिए, करीब दोगुना कोड लिखना पड़ता है. हालांकि, इससे भी ज़्यादा ज़रूरी बात यह है कि इसे समझना आसान नहीं होता. हालांकि, प्रॉमिस के लिए यह कहानी खत्म नहीं होती. जब इन्हें ES6 की अन्य सुविधाओं के साथ जोड़ा जाता है, तो ये और भी आसान हो जाते हैं.

बोनस राउंड: ज़्यादा सुविधाएं

मैंने यह लेख लिखा था. तब से, प्रॉमिस का इस्तेमाल करने की सुविधा काफ़ी बढ़ गई है. Chrome 55 के बाद से, एसिंक फ़ंक्शन की मदद से प्रॉमिस-आधारित कोड को इस तरह लिखा जा सकता है जैसे कि वह सिंक्रोनस हो. हालांकि, इससे मुख्य थ्रेड ब्लॉक नहीं होती. इस बारे में ज़्यादा जानने के लिए, मेरे एसिंक फ़ंक्शन वाले लेख को पढ़ें. मुख्य ब्राउज़र में, Promises और async फ़ंक्शन, दोनों के लिए बड़े पैमाने पर सहायता उपलब्ध है. आपको MDN के Promise और async function रेफ़रंस में इसकी जानकारी मिल सकती है.

हम ऐनी वैन केस्टरन, डोमेनिक डेनिकोला, टॉम ऐशवर्थ, रेमी शार्प, ऐडी ओस्मानी, आर्थर इवांस, और युताका हिरानो को धन्यवाद देते हैं. इन्होंने इस दस्तावेज़ को पढ़ा और इसमें सुधार करने के सुझाव दिए.

साथ ही, लेख के अलग-अलग हिस्सों को अपडेट करने के लिए, Mathias Bynens का धन्यवाद.