De tool-calling loops van Claude staan erom bekend dat ze verwarrende promise-code genereren in Node.js.
De nieuwe Promise.withResolvers() in Node.js 22 stelt ontwikkelaars in staat om het boilerplate-zware new Promise-patroon te vervangen door een enkele regel die de promise en de bijbehorende resolve/reject-functies direct beschikbaar stelt. Het resultaat is minder vergeten resolve-aanroepen, geen waarschuwingen voor dubbele rejects en een vlakkere control flow die gemakkelijker te testen is en beter standhoudt in serverless-omgevingen.
Waarom het oude patroon de flow verstoort
Wanneer een LLM zoals Claude een tool aanroept, ziet de typische Node-implementatie er als volgt uit:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
Er doen zich drie terugkerende valkuilen voor:
- Vergeten resolve – Als het codepad nooit
resolveaanroept, blijft een Lambda of andere serverless handler hangen tot deze een timeout krijgt, wat de kosten verhoogt. - Dubbele reject – Een error-pad dat
rejecttwee keer aanroept, triggert "unhandled rejection"-waarschuwingen die het proces in strict mode kunnen laten crashen. - Diepe nesting – Elke async-stap nestelt een nieuwe callback binnen de constructor, waardoor de logica versnipperd raakt en unit tests kwetsbaar worden.
Al deze problemen komen voort uit het feit dat de control-functies van de promise opgesloten zitten in de closure van de constructor, waardoor de rest van de code daar weer naar terug moet grijpen.
Promise.withResolvers() in één regel
Node 22 voegt een statische helper toe die een object retourneert met daarin een promise en de twee functies die deze afhandelen (settle):
const { promise, resolve, reject } = Promise.withResolvers();
Nu kan de promise worden doorgegeven aan elk deel van het systeem — een HTTP-handler, een database-listener of een background worker — terwijl de oorspronkelijke aanroeper simpelweg de promise awaitt. Het is niet langer nodig om het hele blok voor de tool-executie in een new Promise-constructor te wikkelen.
Toepassing op de tool-loop van Claude
De workflow van Claude is:
- De LLM geeft een tool-verzoek af.
- Jouw code voert de tool uit (bijv. een API-aanroep, het lezen van een bestand).
- Het resultaat van de tool wordt teruggestuurd naar Claude voor de volgende beurt.
Met withResolvers wordt de loop als volgt vereenvoudigd:
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
}
}
De tool-implementatie hoeft niet langer in een nieuwe promise te worden gewikkeld; deze ontvangt simpelweg resolve en reject. Dit elimineert de drie hierboven genoemde foutmodi.
Productie-instellingen die nog steeds van belang zijn
Zelfs met een schonere promise-structuur lopen real-world agents tegen andere beperkingen aan:
- Timeouts – Het bovenstaande fragment bevat een eenvoudige timer die
rejectaanroept als de tool een drempelwaarde overschrijdt. Pas de duur aan op basis van de SLA-verwachtingen. - Throttling – Wanneer de onderliggende service een throttling-fout teruggeeft (bijv.
ThrottlingExceptionvan Bedrock), vang deze dan op, pauzeer en probeer het opnieuw met exponential back-off. Het resolve/reject-paar blijft hetzelfde; alleen de retry-logica verandert. - Lambda-kosten – Stel in AWS Lambda
callbackWaitsForEmptyEventLoop = falsein. Hiermee geef je de runtime de instructie om de functie te beëindigen zodra de handler is teruggekeerd, zelfs als streams of andere achtergrond-handles nog openstaan. Dit voorkomt dat de functie blijft draaien terwijl de promise elders wordt afgehandeld.
Wanneer de nieuwe helper geen wondermiddel is
Promise.withResolvers() is alleen beschikbaar in Node 22 en later. Projecten die vastzitten aan oudere LTS-versies moeten het patroon ofwel polyfillen of vasthouden aan de klassieke constructor. Polyfills kunnen de API nabootsen, maar bieden niet de voordelen van native performance. Bovendien lost de helper logische bugs niet magisch op: ontwikkelaars moeten er nog steeds voor zorgen dat er voor elke aanroep precies één keer resolve of reject wordt aangeroepen, anders blijft de promise onindamig in de status pending staan.
Waar je op moet letten
- Adoptie door frameworks – Libraries die LLM-agent-loops abstraheren (bijv. open-source Claude-wrappers) beginnen
withResolversaan te bieden als een opt-in-functie. Houd updates in de gaten waarbij dit patroon de standaard wordt. - Node-ecosysteem – Naarmate meer services overstappen naar Node 22, zal de helper een de-facto standaard worden voor elk "fire-and-wait" async-patroon, niet alleen voor LLM-agents.
- Standaarden voor tool-calling – Opkomende specificaties voor LLM-tool-aanroepen kunnen een "single-promise"-contract voorschrijven, wat perfect aansluit bij de
withResolvers-aanpak.
Conclusie: Door de omslachtige new Promise-wrapper te vervangen door een eenregelige Promise.withResolvers(), krijgen op Claude gebaseerde agents een duidelijkere flow, minder verrassingen tijdens runtime en meer controle over serverless-kosten — mits de runtime Node 22 ondersteunt.
