Цикли виклику інструментів Claude мають репутацію тих, що породжують заплутаний код промісів у Node.js.
Новий метод Promise.withResolvers() у Node.js 22 дозволяє розробникам замінити переобтяжений шаблонним кодом патерн new Promise одним рядком, який видає проміс разом із функціями resolve та reject. Результатом є менша кількість забутих викликів resolve, відсутність попереджень про подвійний reject та пласкіший потік керування, який легше тестувати та підтримувати в робочому стані в serverless-середовищах.

Чому старий патерн порушує потік керування

Коли LLM, як-от Claude, звертається до інструмента, типова реалізація в Node виглядає так:

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

Виникають три типові пастки:

  • Забутий resolve – якщо шлях коду ніколи не викликає resolve, Lambda або інший serverless-обробник зависає до завершення тайм-ауту, що підвищує витрати.
  • Подвійний reject – шлях помилки, який викликає reject двічі, провокує попередження «unhandled rejection», що може призвести до завершення процесу в строгому режимі (strict mode).
  • Глибока вкладеність – кожен асинхронний крок вкладає ще один callback всередину конструктора, розкидаючи логіку та роблячи юніт-тести крихкими.

Усі ці проблеми виникають через те, що функції керування промісом заблоковані всередині замикання конструктора, що змушує решту коду звертатися назад до нього.

Promise.withResolvers() в один рядок

Node 22 додає статичний допоміжний метод, який повертає об'єкт, що містить проміс і дві функції для його завершення:

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

Тепер проміс можна передати будь-якій частині системи — HTTP-обробнику, слухачу бази даних або фоновому воркеру — тоді як початковий викликаючий просто чекає на проміс за допомогою await. Немає потреби огортати весь блок виконання інструмента в конструктор new Promise.

Застосування до циклу інструментів Claude

Робочий процес Claude виглядає так:

  1. LLM надсилає запит на використання інструмента.
  2. Ваш код виконує інструмент (наприклад, виклик API або читання файлу).
  3. Результат роботи інструмента надсилається назад Claude для наступного кроку.

З withResolvers цикл скорочується до:

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

Реалізацію інструмента більше не потрібно огортати в новий проміс; він просто отримує resolve та reject. Це усуває три вищезгадані сценарії збоїв.

Параметри для продакшену, які все ще важливі

Навіть із чистішою структурою промісів, реальні агенти стикаються з іншими обмеженнями:

  • Тайм-аути – наведений вище фрагмент коду показує простий таймер, який викликає reject, якщо робота інструмента перевищує встановлений поріг. Налаштовуйте тривалість відповідно до очікувань SLA.
  • Тротлінг (Throttling) – коли базовий сервіс повертає помилку обмеження швидкості (наприклад, ThrottlingException у Bedrock), перехопіть її, зробіть паузу та повторіть спробу з експоненціальною затримкою (exponential back-off). Пара resolve/reject залишається незмінним; змінюється лише логіка повторних спроб.
  • Вартість Lambda – в AWS Lambda встановіть callbackWaitsForEmptyEventLoop = false. Це вказує середовищу виконання завершити функцію, як тільки обробник поверне результат, навіть якщо потоки (streams) або інші фонові дескриптори все ще відкриті. Це запобігає зависанню функції, поки проміс очікує завершення в іншому місці.

Коли новий допоміжний метод не є панацеєю

Promise.withResolvers() доступний лише в Node 22 і новіших версіях. Проєкти, що використовують старіші LTS-релізи, мають або використовувати поліфіл (polyfill) для цього патерну, або залишатися на класичному конструкторі. Поліфіли можуть імітувати API, але не дадуть переваг у продуктивності, які забезпечує нативна підтримка. Більше того, цей метод не вирішує логічні помилки магічним чином: розробникам все одно потрібно гарантувати, що для кожного запиту викликається рівно одна функція — або resolve, або reject, інакше проміс залишатиметься в стані pending нескінченно.

На що звернути увагу далі

  • Впровадження у фреймворки – Бібліотеки, що абстрагують цикли LLM-агентів (наприклад, open-source обгортки для Claude), починають пропонувати withResolvers як додаткову функцію. Слідкуйте за оновленнями, які зроблять цей патерн стандартним.
  • Екосистема Node – оскільки все більше сервісів переходитимуть на Node 22, цей метод стане стандартом де-факто для будь-якого асинхронного патерну типу «запустив і чекай» (fire-and-wait), а не лише для LLM-агентів.
  • Стандарти виклику інструментів – Нові специфікації для викликів інструментів LLM можуть передбачати контракт «одного промісу», що ідеально узгоджується з підходом withResolvers.

Підсумок: Замінюючи багатослівну обгортку new Promise на короткий рядок Promise.withResolvers(), агенти на базі Claude отримують чіткіший потік керування, менше непередбачуваних помилок під час виконання та кращий контроль над витратами на serverless — за умови, що середовище виконання підтримує Node 22.