Loop pemanggilan tool Claude memiliki reputasi sering menghasilkan kode promise yang rumit di Node.js.Promise.withResolvers() baru di Node.js 22 memungkinkan pengembang untuk mengganti pola new Promise yang penuh dengan boilerplate dengan satu baris kode yang langsung memberikan promise beserta fungsi resolve/reject-nya. Hasilnya adalah lebih sedikit pemanggilan resolve yang terlupakan, tidak ada peringatan double-reject, dan alur kontrol yang lebih datar sehingga lebih mudah diuji dan dijaga tetap berjalan di lingkungan serverless.
Mengapa pola lama merusak alur kerja
Ketika LLM seperti Claude meminta sebuah tool, implementasi Node yang umum terlihat seperti ini:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
Tiga jebakan yang sering muncul adalah:
- Resolve yang terlupakan – Jika jalur kode tidak pernah memanggil
resolve, handler Lambda atau serverless lainnya akan menggantung hingga timeout, yang akhirnya meningkatkan biaya. - Double reject – Jalur error yang memanggil
rejectdua kali akan memicu peringatan “unhandled rejection” yang dapat menyebabkan proses crash dalam strict mode. - Nesting yang dalam – Setiap langkah async menyisipkan callback lain di dalam constructor, sehingga logika tersebar dan membuat unit test menjadi rapuh.
Semua masalah tersebut bersumber dari fakta bahwa fungsi kontrol promise terkunci di dalam closure constructor, sehingga memaksa sisa kode lainnya untuk mengaksesnya kembali.
Promise.withResolvers() dalam satu baris
Node 22 menambahkan helper statis yang mengembalikan objek berisi promise dan dua fungsi untuk menyelesaikannya (settle):
const { promise, resolve, reject } = Promise.withResolvers();
Sekarang promise tersebut dapat diserahkan ke bagian sistem mana pun—handler HTTP, listener database, atau background worker—sementara pemanggil aslinya cukup melakukan await pada promise tersebut. Tidak perlu lagi membungkus seluruh blok eksekusi tool dalam constructor new Promise.
Menerapkannya pada loop tool Claude
Alur kerja Claude adalah:
- LLM mengeluarkan permintaan tool.
- Kode Anda menjalankan tool (misalnya, panggilan API, pembacaan file).
- Hasil tool dikirim kembali ke Claude untuk giliran berikutnya.
Dengan withResolvers, loop tersebut menjadi lebih ringkas:
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
}
}
Implementasi tool tidak perlu lagi dibungkus dalam promise baru; ia hanya menerima resolve dan reject. Hal ini menghilangkan tiga mode kegagalan yang disebutkan di atas.
Pengaturan produksi yang tetap penting
Meskipun bentuk promise lebih bersih, agen di dunia nyata tetap menghadapi kendala lain:
- Timeouts – Cuplikan kode di atas menunjukkan timer sederhana yang melakukan reject jika tool melampaui ambang batas. Sesuaikan durasi berdasarkan ekspektasi SLA.
- Throttling – Ketika layanan dasar mengembalikan error throttling (misalnya,
ThrottlingExceptiondari Bedrock), tangkap error tersebut, jeda, dan coba lagi dengan exponential back-off. Pasangan resolve/reject tetap sama; hanya logika retry yang berubah. - Biaya Lambda – Di AWS Lambda, atur
callbackWaitsForEmptyEventLoop = false. Ini memberitahu runtime untuk menghentikan fungsi segera setelah handler selesai, meskipun stream atau handle background lainnya masih terbuka. Ini mencegah fungsi tetap berjalan (lingering) saat promise diselesaikan di tempat lain.
Ketika helper baru bukan solusi ajaib
Promise.withResolvers() hanya tersedia di Node 22 dan versi setelahnya. Proyek yang masih menggunakan rilis LTS lama harus menggunakan polyfill untuk pola ini atau tetap menggunakan constructor klasik. Polyfill dapat meniru API tersebut tetapi tidak akan mendapatkan manfaat performa native. Selain itu, helper ini tidak secara ajaib menyelesaikan bug logika: pengembang tetap harus memastikan bahwa tepat satu dari resolve atau reject dipanggil untuk setiap permintaan, jika tidak, promise akan tetap dalam status pending tanpa batas.
Apa yang perlu diperhatikan selanjutnya
- Adopsi framework – Library yang mengabstraksi loop agen LLM (misalnya, wrapper Claude open-source) mulai mengekspos
withResolverssebagai fitur opsional. Pantau pembaruan yang menjadikan pola ini sebagai default. - Ekosistem Node – Seiring semakin banyak layanan beralih ke Node 22, helper ini akan menjadi standar de-facto untuk pola async “fire-and-wait” apa pun, tidak hanya untuk agen LLM.
- Standar pemanggilan tool – Spesifikasi yang muncul untuk pemanggilan tool LLM mungkin menetapkan kontrak “single-promise”, yang selaras sempurna dengan pendekatan
withResolvers.
Kesimpulan: Dengan mengganti pembungkus new Promise yang bertele-tele dengan satu baris Promise.withResolvers(), agen berbasis Claude mendapatkan alur yang lebih jelas, lebih sedikit kejutan saat runtime, dan kontrol yang lebih ketat terhadap biaya serverless—asalkan runtime mendukung Node 22.
