Цикли виклику інструментів 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 виглядає так:
- LLM надсилає запит на використання інструмента.
- Ваш код виконує інструмент (наприклад, виклик API або читання файлу).
- Результат роботи інструмента надсилається назад 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.
