Los bucles de llamada a herramientas (tool-calling) de Claude tienen la reputación de generar código de promesas enredado en Node.js.
La nueva función Promise.withResolvers() de Node.js 22 permite a los desarrolladores sustituir el patrón new Promise, cargado de código repetitivo (boilerplate), por una sola línea que entrega la promesa junto con sus funciones resolve y reject. El resultado es un menor número de llamadas a resolve olvidadas, sin advertencias de doble reject y un flujo de control más plano que es más fácil de probar y mantener activo en entornos serverless.

Por qué el patrón antiguo rompe el flujo

Cuando un LLM como Claude solicita una herramienta, la implementación típica de Node se ve así:

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

Surgen tres errores recurrentes:

  • Resolve olvidado – Si la ruta del código nunca llama a resolve, una Lambda u otro manejador serverless se queda colgado hasta que agota el tiempo de espera (timeout), lo que aumenta los costes.
  • Doble reject – Una ruta de error que llama a reject dos veces activa advertencias de "unhandled rejection" que pueden interrumpir el proceso en modo estricto.
  • Anidamiento profundo – Cada paso asíncrono anida otro callback dentro del constructor, dispersando la lógica y haciendo que las pruebas unitarias sean frágiles.

Todos estos problemas derivan del hecho de que las funciones de control de la promesa están bloqueadas dentro del closure del constructor, lo que obliga al resto del código a intentar acceder a ellas de forma indirecta.

Promise.withResolvers() en una sola línea

Node 22 añade un método estático de ayuda que devuelve un objeto que contiene una promesa y las dos funciones que la resuelven:

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

Ahora la promesa puede entregarse a cualquier parte del sistema —un manejador HTTP, un escuchador de base de datos o un trabajador en segundo plano— mientras que el llamador original simplemente hace await de la promesa. No es necesario envolver todo el bloque de ejecución de la herramienta en un constructor new Promise.

Aplicándolo al bucle de herramientas de Claude

El flujo de trabajo de Claude es:

  1. El LLM emite una solicitud de herramienta.
  2. Tu código ejecuta la herramienta (por ejemplo, una llamada a una API, una lectura de archivo).
  3. El resultado de la herramienta se envía de vuelta a Claude para el siguiente turno.

Con withResolvers, el bucle se reduce a:

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

La implementación de la herramienta ya no necesita estar envuelta en una nueva promesa; simplemente recibe resolve y reject. Esto elimina los tres modos de fallo enumerados anteriormente.

Ajustes de producción que siguen siendo importantes

Incluso con una estructura de promesa más limpia, los agentes del mundo real se enfrentan a otras limitaciones:

  • Timeouts – El fragmento de código anterior muestra un temporizador simple que ejecuta un reject si la herramienta supera un umbral. Ajusta la duración según las expectativas de los SLA.
  • Throttling – Cuando el servicio subyacente devuelve un error de estrangulamiento (por ejemplo, el ThrottlingException de Bedrock), captúralo, haz una pausa y reintenta con un back-off exponencial. El par resolve/reject sigue siendo el mismo; solo cambia la lógica de reintento.
  • Coste de Lambda – En AWS Lambda, establece callbackWaitsForEmptyEventLoop = false. Esto indica al entorno de ejecución que termine la función tan pronto como el manejador retorne, incluso si hay flujos (streams) u otros manejadores en segundo plano que siguen abiertos. Esto evita que la función permanezca activa mientras la promesa se resuelve en otro lugar.

Cuando el nuevo ayudante no es una solución mágica

Promise.withResolvers() solo está disponible en Node 22 y versiones posteriores. Los proyectos limitados a versiones LTS anteriores deben aplicar un polyfill al patrón o seguir utilizando el constructor clásico. Los polyfills pueden imitar la API, pero no obtendrán los beneficios de rendimiento nativo. Además, el ayudante no resuelve mágicamente los errores lógicos: los desarrolladores aún deben asegurarse de que se llame exactamente a uno de los dos (resolve o reject) para cada solicitud, de lo contrario, la promesa permanecerá pendiente indefinidamente.

Qué observar a continuación

  • Adopción de frameworks – Las librerías que abstraen los bucles de agentes de LLM (por ejemplo, wrappers de código abierto para Claude) están empezando a exponer withResolvers como una característica opcional (opt-in). Mantente atento a las actualizaciones que conviertan este patrón en el predeterminado.
  • Ecosistema de Node – A medida que más servicios migren a Node 22, este método se convertirá en un estándar de facto para cualquier patrón asíncrono de tipo "disparar y esperar" (fire-and-wait), no solo para agentes de LLM.
  • Estándares de llamada a herramientas – Las especificaciones emergentes para las llamadas a herramientas de LLM podrían prescribir un contrato de "promesa única", lo cual se alinea perfectamente con el enfoque de withResolvers.

Conclusión: Al sustituir el verboso envoltorio new Promise por una sola línea con Promise.withResolvers(), los agentes basados en Claude obtienen un flujo más claro, menos sorpresas en tiempo de ejecución y un control más estricto sobre los costes de serverless, siempre que el entorno de ejecución sea compatible con Node 22.