Claude ના tool-calling loops Node.js માં ગૂંચવણભર્યા promise code પેદા કરવા માટે જાણીતા છે.
Node.js 22 નું નવું Promise.withResolvers() ડેવલપર્સને boilerplate-heavy new Promise પેટર્નને બદલે એક જ લાઇન દ્વારા promise અને તેના resolve/reject functions મેળવવાની સુવિધા આપે છે. પરિણામે, ભૂલથી resolve કોલ કરવાનું રહી જવું, double-reject ચેતવણીઓ ન આવવી, અને એક સરળ control flow મળે છે જે serverless environments માં ટેસ્ટ કરવામાં અને ચાલુ રાખવામાં સરળ છે.

જૂની પેટર્ન ફ્લોને કેમ તોડે છે

જ્યારે Claude જેવું LLM કોઈ tool ને પૂછે છે, ત્યારે સામાન્ય Node implementation આ મુજબ દેખાય છે:

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

ત્રણ વારંવાર આવતી મુશ્કેલીઓ (pitfalls) ઉભી થાય છે:

  • Forgotten resolve – જો કોડ પાથ ક્યારેય resolve ને કોલ ન કરે, તો Lambda અથવા અન્ય serverless handler timeout ન થાય ત્યાં સુધી અટકી જાય છે, જેનાથી ખર્ચ વધે છે.
  • Double reject – એરર પાથ જે બે વાર reject કોલ કરે છે તે “unhandled rejection” ચેતવણીઓ આપે છે, જે strict mode માં પ્રોસેસને ક્રેશ કરી શકે છે.
  • Deep nesting – દરેક async સ્ટેપ constructor ની અંદર બીજું callback ને નેસ્ટ કરે છે, જેનાથી લોજિક વિખરાઈ જાય છે અને unit tests નબળા પડે છે.

આ બધી સમસ્યાઓનું કારણ એ છે કે promise ના control functions constructor ના closure ની અંદર લોક હોય છે, જેના કારણે બાકીના કોડને તેમાં પાછા પહોંચવું પડે છે.

એક જ લાઇનમાં Promise.withResolvers()

Node 22 એક static helper ઉમેરે છે જે એક object રિટર્ન કરે છે જેમાં promise અને તેને settle કરવા માટેના બે functions હોય છે:

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 માં તેનો ઉપયોગ

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 માટેના મહત્વના પાસાઓ (knobs)

promise નો આકાર વધુ સ્વચ્છ હોવા છતાં, વાસ્તવિક દુનિયાના agents અન્ય મર્યાદાઓનો સામનો કરે છે:

  • Timeouts – ઉપરનો સ્નિપેટ એક સાદો timer બતાવે છે જે જો tool નિર્ધારિત મર્યાદાથી વધી જાય તો reject કરે છે. SLA અપેક્ષાઓના આધારે સમયગાળો સેટ કરો.
  • Throttling – જ્યારે અન્ડરલાઇંગ સર્વિસ throttling error (દા.ત., Bedrock નું ThrottlingException) રિટર્ન કરે, ત્યારે તેને catch કરો, થોભો, અને exponential back-off સાથે ફરી પ્રયાસ કરો. 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 ની નકલ કરી શકે છે પરંતુ નેટિવ પરફોર્મન્સના ફાયદા નહીં મળે. વધુમાં, આ helper જાદુઈ રીતે logical bugs ને ઉકેલતું નથી: ડેવલપર્સને હજુ પણ એ સુનિશ્ચિત કરવાની જરૂર છે કે દરેક request માટે resolve અથવા reject માંથી બરાબર એક જ કોલ કરવામાં આવે, અન્યથા promise અનિશ્ચિત સમય માટે 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 પેટર્ન માટે de-facto standard બની જશે.
  • Tool-calling standards – LLM tool calls માટે ઉભરી રહેલા specifications “single-promise” કોન્ટ્રાક્ટ સૂચવી શકે છે, જે withResolvers અભિગમ સાથે સંપૂર્ણ રીતે સુસંગત છે.

Takeaway: લાંબા new Promise wrapper ને બદલે એક લાઇનના Promise.withResolvers() નો ઉપયોગ કરીને, Claude-આધારિત agents ને વધુ સ્પષ્ટ flow, ઓછા runtime surprises, અને serverless costs પર વધુ સારું નિયંત્રણ મળે છે—જો runtime Node 22 ને સપોર્ટ કરતું હોય તો.