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