I loop di tool-calling di Claude hanno la reputazione di generare codice promise intricato in Node.js.
Promise.withResolvers() di Node.js 22 permette agli sviluppatori di sostituire il pattern new Promise, spesso pesante in termini di boilerplate, con una singola riga che fornisce la promise e le sue funzioni resolve/reject. Il risultato è un numero inferiore di chiamate resolve dimenticate, nessun avviso di doppio reject e un flusso di controllo più lineare, più facile da testare e da mantenere attivo in ambienti serverless.

Perché il vecchio pattern interrompe il flusso

Quando un LLM come Claude richiede l'uso di un tool, l'implementazione tipica in Node è la seguente:

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

Si riscontrano tre problemi ricorrenti:

  • Resolve dimenticato – Se il percorso del codice non chiama mai resolve, una Lambda o un altro handler serverless rimane in sospeso fino al timeout, facendo lievitare i costi.
  • Doppio reject – Un percorso di errore che chiama reject due volte attiva avvisi di "unhandled rejection" che possono far crashare il processo in strict mode.
  • Nidificazione profonda – Ogni passaggio asincrono annida un altro callback all'interno del costruttore, disperdendo la logica e rendendo i test unitari fragili.

Tutti questi problemi derivano dal fatto che le funzioni di controllo della promise sono bloccate all'interno della closure del costruttore, costringendo il resto del codice a richiamarle da lì.

Promise.withResolvers() in una riga

Node 22 aggiunge un helper statico che restituisce un oggetto contenente una promise e le due funzioni che la risolvono (settle):

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

Ora la promise può essere passata a qualsiasi parte del sistema — un handler HTTP, un listener del database o un worker in background — mentre il chiamante originale si limita ad aspettare (await) la promise. Non c'è bisogno di avvolgere l'intero blocco di esecuzione del tool in un costruttore new Promise.

Applicarlo al loop dei tool di Claude

Il workflow di Claude è:

  1. L'LLM emette una richiesta di tool.
  2. Il tuo codice esegue il tool (ad es. una chiamata API, la lettura di un file).
  3. Il risultato del tool viene inviato nuovamente a Claude per il turno successivo.

Con withResolvers, il loop si semplifica in:

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
  }
}

L'implementazione del tool non deve più essere avvolta in una nuova promise; riceve semplicemente resolve e reject. Questo elimina i tre modi di fallimento elencati sopra.

Parametri di produzione che contano ancora

Anche con una struttura della promise più pulita, gli agenti nel mondo reale si scontrano con altri vincoli:

  • Timeout – Lo snippet sopra mostra un semplice timer che esegue il reject se il tool supera una determinata soglia. Regola la durata in base alle aspettative degli SLA.
  • Throttling – Quando il servizio sottostante restituisce un errore di throttling (ad es. ThrottlingException di Bedrock), intercettalo, metti in pausa e riprova con un exponential back-off. La coppia resolve/reject rimane la stessa; cambia solo la logica di retry.
  • Costi Lambda – In AWS Lambda, imposta callbackWaitsForEmptyEventLoop = false. Questo comunica al runtime di terminare la funzione non appena l'handler ritorna, anche se gli stream o altri handle in background sono ancora aperti. Impedisce alla funzione di rimanere in sospeso mentre la promise viene risolta altrove.

Quando il nuovo helper non è la soluzione magica

Promise.withResolvers() è disponibile solo in Node 22 e versioni successive. I progetti vincolati a versioni LTS più vecchie devono utilizzare un polyfill per il pattern o attenersi al costruttore classico. I polyfill possono emulare l'API ma non offriranno i vantaggi delle prestazioni native. Inoltre, l'helper non risolve magicamente i bug logici: gli sviluppatori devono comunque assicurarsi che venga chiamata esattamente una tra resolve o reject per ogni richiesta, altrimenti la promise rimarrà in sospeso indefinitamente.

Cosa monitorare in seguito

  • Adozione dei framework – Le librerie che astraggono i loop degli agenti LLM (ad es. wrapper open-source per Claude) stanno iniziando a esporre withResolvers come funzionalità opzionale. Tieni d'occhio gli aggiornamenti che renderanno questo pattern lo standard predefinito.
  • Ecosistema Node – Man mano che sempre più servizi passeranno a Node 22, l'helper diventerà uno standard de facto per qualsiasi pattern asincrono di tipo "fire-and-wait", non solo per gli agenti LLM.
  • Standard per il tool-calling – Le specifiche emergenti per le chiamate ai tool degli LLM potrebbero prescrivere un contratto a "singola promise", che si allinea perfettamente con l'approccio withResolvers.

In sintesi: Sostituendo il verboso wrapper new Promise con la riga singola Promise.withResolvers(), gli agenti basati su Claude ottengono un flusso più chiaro, meno sorprese a runtime e un controllo più stretto sui costi serverless, a patto che il runtime supporti Node 22.