Claude च्या tool-calling loops मुळे Node.js मध्ये गुंतागुंतीचा (tangled) promise code तयार होण्याची शक्यता असते.
Node.js 22 मधील नवीन Promise.withResolvers() मुळे डेव्हलपर्सना boilerplate-heavy new Promise पॅटर्नच्या जागी एक सिंगल लाईन वापरता येते, जी promise आणि तिची resolve/reject फंक्शन्स एकाच वेळी देते. याचा परिणाम म्हणजे विसरलेले resolve कॉल्स कमी होतात, double-reject चे इशारे (warnings) येत नाहीत आणि control flow अधिक सुटसुटीत होतो, ज्यामुळे तो टेस्ट करणे आणि serverless वातावरणात चालू ठेवणे सोपे जाते.

जुना पॅटर्न फ्लो (flow) का बिघडवतो

जेव्हा Claude सारखे LLM एखाद्या tool ला विनंती करते, तेव्हा typical Node implementation असे दिसते:

return new Promise((resolve, reject) => {
  // launch the tool, attach callbacks, maybe fire another async call
});

यामध्ये तीन वारंवार येणाऱ्या समस्या (pitfalls) उद्भवतात:

  • Forgotten resolve – जर कोड पाथने कधीही resolve कॉल केला नाही, तर Lambda किंवा इतर serverless handler टाइमआउट होईपर्यंत अडकून पडतो (hangs), ज्यामुळे खर्च वाढतो.
  • Double reject – जर एखादा error path दोनदा reject कॉल करत असेल, तर “unhandled rejection” चे warnings मिळतात, ज्यामुळे strict mode मध्ये process क्रॅश होऊ शकते.
  • Deep nesting – प्रत्येक async स्टेप constructor च्या आत आणखी एक callback नेस्ट करते, ज्यामुळे लॉजिक विखुरले जाते आणि unit tests कमकुवत (brittle) होतात.

या सर्व समस्यांचे मूळ कारण असे आहे की, promise ची control functions constructor च्या closure मध्ये लॉक असतात, ज्यामुळे उर्वरित कोडला पुन्हा त्याकडे जावे लागते.

Promise.withResolvers() एका सिंगल लाईनमध्ये

Node 22 मध्ये एक static helper जोडण्यात आले आहे, जे एक object रिटर्न करते ज्यामध्ये promise आणि त्याला settle करणारी दोन फंक्शन्स असतात:

const { promise, resolve, reject } = Promise.withResolvers();

आता promise सिस्टिमच्या कोणत्याही भागाकडे सोपवता येतो—मग तो HTTP handler असो, database listener असो किंवा background worker—आणि मूळ caller फक्त promise ला await करू शकतो. संपूर्ण tool-execution ब्लॉकला new Promise constructor मध्ये गुंडाळण्याची (wrap करण्याची) आता गरज नाही.

Claude च्या tool loop ला याचे उपयोजन (Applying)

Claude चा workflow असा आहे:

  1. LLM एक tool request पाठवते.
  2. तुमचा कोड tool चालवतो (उदा. API call, file read).
  3. tool चा निकाल पुढच्या टर्नसाठी Claude कडे परत पाठवला जातो.

withResolvers मुळे, हा loop खालीलप्रमाणे सोपा होतो:

async function runTool(request) {
  const { promise, resolve, reject } = Promise.withResolvers();

  // Kick off the tool; it can call resolve/reject from anywhere
  executeTool(request, { resolve, reject });

  // Optional timeout wrapper
  const timeout = setTimeout(() => reject(new Error('Tool timed out')), 10_000);
  try {
    const result = await promise;
    clearTimeout(timeout);
    return result;               // feed back to Claude
  } finally {
    // clean-up if needed
  }
}

tool implementation ला आता नवीन promise मध्ये wrap करण्याची गरज नाही; त्याला फक्त resolve आणि reject मिळते. यामुळे वर नमूद केलेले तीन failure modes दूर होतात.

प्रोडक्शनमध्ये (Production) महत्त्वाचे घटक

promise चा आकार अधिक सुटसुटीत झाला तरी, रिअल-वर्ल्ड एजंट्सना इतर मर्यादांचा सामना करावा लागतो:

  • Timeouts – वरील snippet मध्ये एक साधा timer दाखवला आहे जो tool ने ठराविक मर्यादेपेक्षा जास्त वेळ घेतल्यास reject करतो. SLA च्या अपेक्षेनुसार कालावधी (duration) समायोजित करा.
  • Throttling – जेव्हा मूळ service throttling error (उदा. Bedrock चा ThrottlingException) देते, तेव्हा तो catch करा, थोडा वेळ थांबा आणि exponential back-off सह पुन्हा प्रयत्न (retry) करा. resolve/reject जोडी तीच राहते; फक्त retry logic बदलते.
  • Lambda cost – AWS Lambda मध्ये, callbackWaitsForEmptyEventLoop = false सेट करा. यामुळे runtime ला असे सांगता येते की handler परत आल्यावर लगेच function थांबवावे, जरी streams किंवा इतर background handles अजूनही उघडे असले तरीही. यामुळे promise इतरत्र settle होत असताना function जास्त वेळ चालू राहण्यापासून वाचते.

जेव्हा नवीन helper हा सर्व समस्यांवर रामबाण उपाय (silver bullet) नसतो

Promise.withResolvers() फक्त Node 22 आणि त्यानंतरच्या आवृत्त्यांमध्ये उपलब्ध आहे. जुन्या LTS releases वर आधारित प्रोजेक्ट्सना एकतर या पॅटर्नसाठी polyfill वापरावा लागेल किंवा क्लासिक constructor चा वापर करावा लागेल. Polyfills API ची नक्कल करू शकतात पण त्यांना native performance चा फायदा मिळणार नाही. शिवाय, हे helper लॉजिकल bugs जादूने सोडवत नाही: प्रत्येक request साठी resolve किंवा reject पैकी नेमके एकच कॉल केले जाईल याची खात्री डेव्हलपर्सना करावी लागेल, अन्यथा promise अनिश्चित काळासाठी (indefinitely) pending राहील.

पुढे काय पाहावे

  • Framework adoption – LLM agent loops abstract करणाऱ्या लायब्ररीज (उदा. open-source Claude wrappers) withResolvers ला एक opt-in feature म्हणून उपलब्ध करून देऊ लागल्या आहेत. या पॅटर्नला default बनवणाऱ्या अपडेट्सवर लक्ष ठेवा.
  • Node ecosystem – जशा अधिक सेवा Node 22 वर स्थलांतरित होतील, तसे हे helper केवळ LLM agents साठीच नाही, तर कोणत्याही “fire-and-wait” async pattern साठी एक de-facto standard बनेल.
  • Tool-calling standards – LLM tool calls साठी येत असलेल्या नवीन specifications मध्ये “single-promise” कराराचा (contract) उल्लेख असू शकतो, जो withResolvers दृष्टिकोनाशी पूर्णपणे सुसंगत आहे.

Takeaway: verbose new Promise wrapper च्या जागी एक-लाईन Promise.withResolvers() वापरल्यामुळे, Claude-आधारित एजंट्सना अधिक स्पष्ट flow, कमी runtime surprises आणि serverless खर्चावर अधिक नियंत्रण मिळते—जर runtime Node 22 ला सपोर्ट करत असेल तर.