Явное управление ресурсами в JavaScript: using, await using и DisposableStack

Мы пишем код, который что-то открывает: файл, поток, блокировку, соединение с базой. А ещё мы должны это закрыть. Язык наконец берёт это на себя.

JavaScript всегда делал управление ресурсами нашей головной болью. Мы годами строили паттерны: dispose, teardown, кастомную очистку в библиотеках. У каждого решения было своё имя, свой контракт и ноль гарантий от языка. Мы лезли в try/finally и надеялись, что не пропустили краевой случай.

Это меняется.

Явное управление ресурсами даёт JavaScript встроенный, языковой способ сказать: «Этой штуке нужна уборка, и рантайм гарантирует, что она случится». Не как соглашение, а как часть языка.

Код, который легко сломать

Так мы пишем сейчас:

const file = await openFile("data.txt");

try {
// работаем с файлом
} finally {
await file.close();
}

Код работает. Но у него две проблемы.

Первая: он многословный и однообразный. На каждый ресурс — свой try/finally. При рефакторинге легко забыть обернуть новый участок кода или переставить строки в finally.

Вторая — неочевидная: если ресурсов два, важен порядок очистки.

const file = await openFile("data.txt");
const lock = await acquireLock();

try {
// работаем с файлом и блокировкой
} finally {
await lock.release(); // если поменять местами -
await file.close(); // можно получить дедлок
}

Синтаксис не подсказывает, что порядок важен. Язык не помогает, а полагаться на дисциплину — рискованно.

using: ресурс, который закрывается сам

С новой фичей тот же код выглядит так:

using file = await openFile("data.txt");
using lock = await acquireLock();

// работаем с файлом и блокировкой
// file и lock автоматически закроются при выходе из области видимости

Никакого try. Никакого finally. Никакого «я не уверен, закрыл ли».

Объявление using file = ... привязывает время жизни переменной к текущему блоку. Как только выполнение дойдёт до }, переменная станет недоступна — и сразу после этого будет вызван Symbol.dispose. Это гарантия языка, а не особенность реализации

Как это работает: контракт, а не магия

Чтобы объект можно было использовать с using, он должен реализовать метод с ключом Symbol.dispose:

class FileHandle {
async write(data) { /* ... */ }

[Symbol.dispose]() {
this.close(); // синхронная очистка
}
}

Для асинхронной очистки — Symbol.asyncDispose:

class FileHandle {
async write(data) { /* ... */ }

async [Symbol.asyncDispose]() {
await this.close();
}
}

Это всё. using не умеет магически закрывать файлы — он умеет вызывать метод, который вы сами написали. Но теперь этот метод стандартизован. Библиотекам больше не нужно придумывать собственные имена (.close(), .release(), .destroy()), а разработчикам — запоминать их.

Важно: using — для синхронной очистки, await using — для асинхронной. Та же граница, что и везде в JS: forEach vs map, get vs await get.

Время жизни = область видимости

using работает как const или let:

{
await using file = await openFile("data.txt");
// file доступен только здесь
}
// file закрыт

Это не просто синтаксис, а визуализация времени жизни. Раньше ресурс жил до вызова .close() где-то внизу функции — найти этот вызов можно было только поиском. Теперь граница видна прямо в коде.

Когда using не хватает: DisposableStack

using требует, чтобы ресурс был объявлен статически, в фиксированном блоке. А если мы не знаем заранее, сколько ресурсов понадобится?

// ❌ Так не работает - переменная живёт одну итерацию
for (const name of files) {
using f = await openFile(name); // ошибка или не то поведение
}

Нужно, чтобы все файлы открылись в цикле, а закрылись после него, все вместе. Для этого есть AsyncDisposableStack:

const stack = new AsyncDisposableStack();

for (const fileName of files) {
stack.use(await openFile(fileName)); // добавляем в стек
}

if (featureFlags.enableLock) {
stack.use(await acquireLock());
}

// работаем со всеми ресурсами

await stack.disposeAsync(); // очистит всё в обратном порядке

DisposableStack даёт те же гарантии, что и using: при вызове .disposeAsync() все ресурсы будут очищены, и если один из них выбросит ошибку — остальные всё равно будут закрыты. Разница лишь в том, что using делает это автоматически при выходе из блока, а стек требует явного вызова.

Что можно сделать уже сегодня

Статус поддержки на начало 2026:

  • Chrome 123+
  • Firefox 119+
  • Node.js 20.9+
  • Safari — в разработке (доступен под флагом)

Прямо сейчас в консоли браузера:

{
class Logger {
log(msg) { console.log(msg); }
[Symbol.dispose]() { console.log('✅ cleanup'); }
}

using log = new Logger();
log.log('Привет!');
}
// → Привет!
// → ✅ cleanup

Но это игрушечный пример. Вот три сценария, которые можно внедрять в реальный код уже сегодня.

Автоотмена fetch при выходе
function abortableFetch(url) {
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal });

return {
promise,
[Symbol.dispose]: () => controller.abort()
};
}

{
using { promise } = abortableFetch('https://api.example.com/data');
const response = await promise;
// Если произойдёт ошибка или мы выйдем из блока раньше - fetch отменится
}
Блокировки без утечек
navigator.locks.request('my-resource', async (lock) => {
// Встроенный API неудобен: весь код - внутри колбэка
});

// Оборачиваем в disposable
async function withLock(name) {
const lock = await navigator.locks.request(name);
return {
...lock,
[Symbol.asyncDispose]: () => lock.release()
};
}

{
await using lock = await withLock('my-resource');
// Весь код пишем здесь, без лишней вложенности
}
Полифилл для старых окружений

TypeScript поддерживает using с версии 5.2. Достаточно включить target: ESNext или поставить tslib. Для работы в старых браузерах можно использовать полифилл:

import 'disposablestack/auto';

Это не только для сервера

Кажется, что фича про бэкенд и файлы. Но на фронтенде — те же проблемы:

  • Веб-стримы нужно закрывать
  • navigator.locks — освобождать
  • Подписки на события — отписывать
  • Транзакции IndexedDB — завершать

Раньше мы решали это соглашениями: subscribe() / unsubscribe(), open() / close(). Теперь у языка есть единый контракт. Он не исправляет старый код, но даёт возможность писать новый — безопаснее и чище.

Заключение

using и await using — не революция. Это стандартизация того, что мы и так делали. Но именно такие изменения превращают языки из тех, с которыми «можно работать», в те, в которых «сложно ошибиться».

Спецификация Stage 3, реализация в двух движках из трёх, полифилл для остальных. using уже можно использовать в продакшене при целевой аудитории с современными браузерами. Через год-два это станет стандартом де-факто — как async/await или class

Комментарии


Дополнительные материалы

Предыдущая Статья

Анимация элемента dialog

Следующая Статья

Как закоммитить многострочные сообщения в git commit