Claude کے tool-calling loops کی وجہ سے Node.js میں اکثر الجھا ہوا promise code بن جاتا ہے۔
Node.js 22 کا نیا Promise.withResolvers() ڈویلپرز کو boilerplate سے بھرے new Promise پیٹرن کی جگہ ایک ایسی سنگل لائن استعمال کرنے کی اجازت دیتا ہے جو promise اور اس کے resolve/reject فنکشنز فراہم کرتی ہے۔ اس کا نتیجہ یہ ہے کہ resolve کالز بھولنے کے امکانات کم ہو جاتے ہیں، double-reject وارننگز نہیں آتی ہیں، اور کنٹرول فلو زیادہ سادہ (flatter) ہو جاتا ہے جسے serverless ماحول میں ٹیسٹ کرنا اور برقرار رکھنا آسان ہوتا ہے۔

پرانا پیٹرن فلو کو کیوں خراب کرتا ہے

جب 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 ٹائم آؤٹ ہونے تک ہینگ رہتا ہے، جس سے لاگت (cost) بڑھ جاتی ہے۔
  • Double reject – اگر کوئی error path دو بار reject کو کال کرتا ہے، تو یہ "unhandled rejection" وارننگز پیدا کرتا ہے جو strict mode میں پروسیس کو کریش کر سکتی ہے۔
  • Deep nesting – ہر async مرحلہ کنسٹرکٹر کے اندر ایک اور callback کو شامل (nest) کرتا ہے، جس سے لاجک بکھر جاتی ہے اور unit tests کمزور (brittle) ہو جاتے ہیں۔

یہ تمام مسائل اس حقیقت سے پیدا ہوتے ہیں کہ promise کے کنٹرول فنکشنز کنسٹرکٹر کے closure کے اندر بند ہوتے ہیں، جس کی وجہ سے باقی کوڈ کو دوبارہ ان تک رسائی حاصل کرنے کے لیے مجبور ہونا پڑتا ہے۔

Promise.withResolvers() ایک لائن میں

Node 22 ایک static helper شامل کرتا ہے جو ایک ایسا object واپس کرتا ہے جس میں ایک promise اور اسے مکمل کرنے والے دو فنکشنز شامل ہوتے ہیں:

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

اب promise کو سسٹم کے کسی بھی حصے—جیسے HTTP handler، database listener، یا background worker—کو سونپا جا سکتا ہے، جبکہ اصل کالر صرف promise کا await کرتا ہے۔ پورے tool-execution بلاک کو new Promise کنسٹرکٹر میں لپیٹنے (wrap کرنے) کی ضرورت نہیں ہے۔

اسے Claude کے tool loop پر لاگو کرنا

Claude کا workflow یہ ہے:

  1. LLM ایک tool request جاری کرتا ہے۔
  2. آپ کا کوڈ tool چلاتا ہے (مثلاً، ایک API call، یا فائل ریڈ کرنا)۔
  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 میں لپیٹنے کی ضرورت نہیں ہے؛ اسے صرف resolve اور reject موصول ہوتے ہیں۔ اس سے اوپر درج تینوں ناکامی کے طریقے (failure modes) ختم ہو جاتے ہیں۔

پروڈکشن کے اہم پہلو (Production knobs) جو اب بھی اہمیت رکھتے ہیں

ایک بہتر promise structure کے باوجود، حقیقی دنیا کے agents کو دیگر رکاوٹوں کا سامنا کرنا پڑتا ہے:

  • Timeouts – اوپر دیا گیا کوڈ ایک سادہ ٹائمر دکھاتا ہے جو tool کے ایک حد (threshold) سے تجاوز کرنے پر reject کر دیتا ہے۔ SLA کی توقعات کے مطابق دورانیہ (duration) کو ایڈجسٹ کریں۔
  • Throttling – جب بنیادی سروس throttling error (مثلاً Bedrock کا ThrottlingException) واپس کرتی ہے، تو اسے catch کریں، تھوڑی دیر رکیں، اور exponential back-off کے ساتھ دوبارہ کوشش کریں۔ resolve/reject کا جوڑا وہی رہتا ہے؛ صرف retry کی لاجک تبدیل ہوتی ہے۔
  • Lambda cost – AWS Lambda میں، callbackWaitsForEmptyEventLoop = false سیٹ کریں۔ یہ runtime کو بتاتا ہے کہ جیسے ہی handler واپس آئے، فنکشن کو ختم کر دیا جائے، چاہے streams یا دیگر background handles ابھی بھی کھلے ہوں۔ یہ فنکشن کو اس وقت تک رکے رہنے سے روکتا ہے جب تک promise کہیں اور مکمل (settle) نہ ہو جائے۔

جب نیا helper ہر مسئلے کا حل (silver bullet) نہ ہو

Promise.withResolvers() صرف Node 22 اور اس کے بعد کے ورژن میں دستیاب ہے۔ پرانے LTS ریلیز پر مبنی پروجیکٹس کو یا تو اس پیٹرن کا polyfill استعمال کرنا ہوگا یا کلاسک کنسٹرکٹر پر ہی قائم رہنا ہوگا۔ Polyfills API کی نقل تو کر سکتے ہیں لیکن وہ native performance کے فوائد حاصل نہیں کر سکیں گے۔ مزید برآں، یہ helper جادوئی طور پر منطقی غلطیوں (logical bugs) کو حل نہیں کرتا: ڈویلپرز کو اب بھی اس بات کو یقینی بنانا ہوگا کہ ہر درخواست کے لیے resolve یا reject میں سے کوئی ایک ہی کال کیا جائے، ورنہ promise غیر معینہ مدت تک pending رہے گا۔

آگے کیا دیکھنا ہے

  • Framework adoption – وہ لائبریریز جو LLM agent loops کو abstract کرتی ہیں (مثلاً open-source Claude wrappers) اب withResolvers کو ایک opt-in فیچر کے طور پر پیش کرنا شروع کر رہی ہیں۔ ان اپ ڈیٹس پر نظر رکھیں جو اس پیٹرن کو ڈیفالٹ بنا دیں۔
  • Node ecosystem – جیسے جیسے زیادہ سروسز Node 22 پر منتقل ہوں گی، یہ helper صرف LLM agents کے لیے ہی نہیں بلکہ کسی بھی "fire-and-wait" async پیٹرن کے لیے ایک de-facto معیار بن جائے گا۔
  • Tool-calling standards – LLM tool calls کے لیے ابھرتی ہوئی وضاحتیں (specifications) ایک "single-promise" معاہدے کی تجویز دے سکتی ہیں، جو withResolvers کے طریقہ کار کے ساتھ مکمل طور پر مطابقت رکھتی ہے۔

خلاصہ (Takeaway): زیادہ تفصیل والے new Promise کے بجائے ایک لائن والے Promise.withResolvers() کو استعمال کر کے، Claude پر مبنی ایجنٹس کو زیادہ واضح فلو، runtime کے کم حیران کن مسائل، اور serverless لاگت پر بہتر کنٹرول حاصل ہوتا ہے—بشرطیکہ runtime Node 22 کو سپورٹ کرتا ہو۔