Como o JavaScript Finalmente Aprendeu a Limpar Após Si Mesmo
Gerenciamento explícito de recursos em JavaScript com using, await using, DisposableStack e SuppressedError para limpeza determinística de arquivos, locks, sockets e conexões.
O novo recurso de Gerenciamento Explícito de Recursos do JavaScript — as declarações using e await using suportadas por Symbol.dispose e Symbol.asyncDispose — oferece à linguagem uma limpeza determinística e baseada em escopo de recursos não relacionados à memória, como identificadores de arquivo, sockets, locks e conexões de banco de dados. Isso não é coleta de lixo. A coleta de lixo recupera memória em seu próprio cronograma não determinístico, e nunca fechou seus arquivos, liberou seus locks ou cancelou a inscrição de seus event listeners. Esses são exatamente os recursos que o using limpa, de forma previsível, no momento em que um escopo é encerrado.
Se você tem escrito blocos try/finally manualmente para fechar conexões e liberar locks, esse recurso substitui esse código repetitivo com uma única palavra-chave. Este artigo explica o que o using faz, como funciona a divisão entre síncrono e assíncrono, como escrever seus próprios disposables, como coordenar vários deles com DisposableStack, como adaptar bibliotecas que ainda não oferecem suporte a isso, e como os erros de descarte são expostos por meio do novo SuppressedError. (WeakRef e FinalizationRegistry são as ferramentas adjacentes à coleta de lixo para memória; este é um mecanismo completamente diferente.)
Principais Conclusões
- Uma declaração
usingchama o método[Symbol.dispose]()de um objeto quando o bloco envolvente é encerrado;await usingchama[Symbol.asyncDispose]()e o aguarda, para que a finalização que retorna uma promise seja concluída antes do fechamento do escopo. - Quando vários recursos compartilham um escopo, eles são descartados na ordem inversa de declaração, de modo que um recurso dependente é finalizado antes da dependência da qual ele depende.
- O Gerenciamento Explícito de Recursos é uma proposta TC39 concluída e padronizada no ES2026, disponível nativamente no Chrome 134 (V8 13.8), Firefox 141 e Node.js 24 (V8 13.6); o Safari ainda está se atualizando — o símbolo
Symbol.disposechegou ao Safari desktop 26.4, mas ainda não ao Safari no iOS. DisposableStack.prototype.move()transfere os recursos registrados para uma nova stack e marca a original como descartada sem descartar nada — o padrão para repassar com segurança recursos parcialmente construídos a partir de um construtor.- Se o descarte lançar uma exceção enquanto seu código já está lançando outra, o JavaScript encapsula ambas em um
SuppressedError, para que nem a falha original nem a falha de limpeza sejam perdidas.
O problema de limpeza que o try/finally nunca resolveu bem
Todo recurso que você abre — um identificador de arquivo, um stream reader, uma conexão de banco de dados, um lock — precisa ser fechado, e a única ferramenta em nível de linguagem para garantir isso era o try/finally. Funciona, mas é verboso e não escala bem. Considere um leitor de Web Streams: se ocorrer um erro durante o processo de leitura e você esquecer de chamar releaseLock() antes que o erro se propague, o stream permanece bloqueado. A solução é envolver o loop de leitura em um bloco try e colocar reader.releaseLock() no finally para que o lock seja sempre liberado.
Esse padrão funciona bem para um único recurso. Com dois ou três recursos interdependentes, ele se transforma em blocos finally aninhados onde a ordem importa e um lançamento dentro de uma limpeza pode ignorar as demais. A parte mecânica — “este objeto precisa de finalização, execute-a na saída, na ordem correta, mesmo no caminho de erro” — é exatamente o que a linguagem agora trata por você.
Como a palavra-chave using funciona
Discover how at OpenReplay.com.
A palavra-chave using declara uma vinculação de escopo de bloco não reatribuível (como const) cujo valor deve ser null, undefined ou um objeto que contenha um método [Symbol.dispose](); quando a variável sai do escopo, esse método é executado automaticamente. Use using para limpeza síncrona. Use await using quando a finalização retornar uma promise — ela declara variáveis de escopo de bloco que são descartadas de forma assíncrona, o valor deve ter um método [Symbol.asyncDispose]() ou [Symbol.dispose](), e esse método é chamado e aguardado quando a variável sai do escopo. Como await using também aceita disposables síncronos simples, você pode utilizá-lo sempre que não souber qual tipo você tem.
import fs from "node:fs/promises";
async function readConfig() {
await using file = await fs.open("config.json", "r");
const { buffer } = await file.read();
return buffer.toString();
// file[Symbol.asyncDispose]() é chamado e aguardado aqui, em todos os caminhos de saída
}
Os objetos FileHandle do Node.js já implementam o protocolo de disposable assíncrono, portanto isso funciona sem um wrapper manual. Observe as duas operações await: await fs.open() aguarda a aquisição e desencapsula a promise em um FileHandle, enquanto await using aguarda o descarte quando a variável sai do escopo.
A regra de ordenação é mais importante quando os recursos dependem uns dos outros. Se um escopo contiver múltiplas declarações using ou await using, todos os disposers são executados em sequência na ordem inversa de declaração, independentemente do tipo de declaração, e todos têm a execução garantida, assim como um bloco finally.
await using db = await openConnection(); // descartado por último
await using tx = await db.beginTransaction(); // descartado primeiro
// tx é finalizado antes de db, para que a conexão ainda esteja ativa quando
// a transação for finalizada
Sobre escopo: using é válido dentro de qualquer bloco, corpo de função, bloco de inicialização estática, cabeçalho for e no nível superior de um módulo — mas não no nível superior de um script, porque os escopos de script persistem e o disposer nunca seria acionado. await using no nível superior é exclusivo de módulos, pois depende de await no nível superior. As regras alinhadas à especificação estão documentadas na referência MDN de using.
Escrevendo seu próprio disposable
Qualquer objeto se torna descartável implementando [Symbol.dispose]() para limpeza síncrona ou [Symbol.asyncDispose]() para limpeza assíncrona — não há classe base ou etapa de registro. O símbolo bem conhecido é o contrato: um objeto é descartável se tiver um método [Symbol.dispose](), e a declaração using busca esse símbolo no inicializador para o método a ser chamado quando a variável sair do escopo.
Aqui está um wrapper de conexão de banco de dados utilizado ao longo do restante deste artigo:
class DatabaseConnection {
#client;
constructor(client) {
this.#client = client;
}
query(sql, params) {
return this.#client.query(sql, params);
}
async [Symbol.asyncDispose]() {
await this.#client.end();
}
}
async function getUser(pool, id) {
await using db = new DatabaseConnection(await pool.acquire());
return await db.query("SELECT * FROM users WHERE id = $1", [id]);
// db[Symbol.asyncDispose]() é executado aqui — o cliente é liberado em todos os caminhos
}
Um disposer síncrono segue o mesmo formato com [Symbol.dispose]() em vez disso. Um disposer síncrono não deve retornar uma promise, pois promises retornadas por [Symbol.dispose]() não são aguardadas pelo await using; para declarar disposables assíncronos, use Symbol.asyncDispose.
Coordenando recursos com DisposableStack
Quando um único objeto possui vários recursos — ou quando você está adquirindo recursos em um loop — DisposableStack e AsyncDisposableStack os coletam e descartam o grupo inteiro de uma vez, em ordem inversa. Ambas as estruturas fornecem métodos como use(), adopt() e defer() para adicionar recursos ou ações de descarte, e um método dispose() ou asyncDispose() para acionar a limpeza. Elas próprias carregam [Symbol.dispose]() / [Symbol.asyncDispose](), portanto funcionam com using e await using.
Os três métodos de registro cobrem diferentes formatos:
| Método | Registra | Use quando |
|---|---|---|
use(resource) | Um recurso que já possui um símbolo de descarte | O objeto é ele próprio descartável |
adopt(value, onDispose) | Um valor não descartável mais um callback de limpeza | O recurso tem um método no estilo close/abort, mas sem símbolo |
defer(onDispose) | Um callback de limpeza independente, sem recurso | Você precisa executar uma finalização arbitrária (ex.: clearInterval) |
function startWorker() {
using stack = new DisposableStack();
const handle = setInterval(poll, 5000);
stack.defer(() => clearInterval(handle));
stack.adopt(openSocket(), (s) => s.close());
// ambas as limpezas são executadas, em ordem inversa, quando o bloco é encerrado
}
O poder não óbvio está no move(), que resolve um problema real de segurança na construção. Às vezes o escopo de função não é suficiente — uma classe ou objeto possui vários recursos que devem ser agrupados como using, mas como um campo de classe ou closure, que é exatamente para isso que servem DisposableStack e AsyncDisposableStack. DisposableStack.prototype.move() transfere todos os recursos registrados para uma nova stack e marca a original como descartada sem descartar os recursos, o que permite construir recursos localmente e repassá-los apenas se a construção for concluída com êxito:
function openResources() {
using cleanup = new DisposableStack();
const a = cleanup.use(openA());
const b = cleanup.use(openB()); // se isso lançar, `a` é descartado automaticamente
const moved = cleanup.move(); // sucesso: a propriedade é transferida, nada é descartado
return moved; // o chamador agora é responsável pelo descarte de ambos
}
Se openB() lançar uma exceção, o bloco é encerrado e cleanup descarta a — sem vazamento. Se tudo for bem-sucedido, move() repassa os recursos ao chamador intactos. A semântica de move() está documentada no artigo sobre Gerenciamento Explícito de Recursos do V8.
Adaptando bibliotecas que ainda não oferecem suporte ao descarte
Você não precisa que uma biblioteca adote Symbol.dispose para usá-la com using — anexe o símbolo você mesmo com Object.assign. Um cliente de biblioteca que expõe um método close() ou end() mas nenhum símbolo de descarte se torna descartável em uma única linha. Atribuir um método Symbol.asyncDispose ao cliente significa que você pode usá-lo em declarações await using e com AsyncDisposableStack#use(), e se você posteriormente atualizar para uma versão que implemente o protocolo nativamente, receberá um erro lembrando-o de remover o shim.
const client = await MongoClient.connect(url);
Object.assign(client, {
async [Symbol.asyncDispose]() {
await client.close();
},
});
async function run() {
await using db = client; // agora descarta corretamente
// ...
}
O shim com Object.assign é a ponte para o ecossistema atual: a maioria dos clientes de terceiros ainda não implementou símbolos de descarte, e ele permite adotá-los hoje mesmo.
Onde isso ajuda no frontend e no Node
No cliente, os recursos que vazam são event listeners, instâncias de IntersectionObserver/ResizeObserver, navigator.locks, Web Streams bloqueados e transações IndexedDB — todos com uma etapa de liberação explícita que é fácil de ignorar em um caminho de erro. Envolvê-los em um disposable vincula seu ciclo de vida a um escopo. O exemplo canônico da equipe do V8 é um stream reader: uma declaração using sobre um objeto cujo [Symbol.dispose]() chama reader.releaseLock() elimina a necessidade de lembrar da liberação por completo.
Observers órfãos e locks não liberados são precisamente os vazamentos que passam despercebidos em um teste local rápido. Um observer órfão ou um lock não liberado não custa nada em uma página que você recarrega a cada poucos segundos, mas ao longo de uma longa sessão de single-page app eles se acumulam em crescimento gradual de memória e travamentos de interação — a degradação lenta que uma reprodução completa de sessão revela quando um simples recarregamento nunca reproduz o problema. Delimitar esses recursos com using é a correção estrutural para essa classe de bugs.
No servidor, os objetos FileHandle do Node.js provenientes de fs/promises e muitos tipos de conexão já participam, e wrappers personalizados como o DatabaseConnection acima cobrem o restante.
Tratando erros durante o descarte com SuppressedError
O descarte pode falhar, e pode falhar enquanto seu código já está lançando uma exceção — sem um comportamento definido, um erro silenciosamente sobrescreveria o outro. O JavaScript resolve isso com SuppressedError. Todos os erros lançados durante o descarte, incluindo o erro inicial que causou a saída do escopo, são agregados dentro de um único SuppressedError — cada exceção anterior como a propriedade suppressed e a exceção posterior como a propriedade error — e ele é lançado após a conclusão do descarte.
Portanto, se seu bloco lançar uma exceção e um disposer também lançar, você captura um único SuppressedError cujo .error é a falha de descarte e cujo .suppressed é o original. Quando múltiplos disposers lançam exceções, elas se aninham, de modo que todas as falhas são acessíveis:
try {
using a = makeDisposableThatThrowsOnDispose("a");
using b = makeDisposableThatThrowsOnDispose("b");
throw new Error("body failed");
} catch (e) {
// e é um SuppressedError; b é descartado primeiro e a por último, então
// e.error é o erro de descarte de a, e e.suppressed aninha o restante
// (o erro de descarte de b, depois o "body failed" original)
}
SuppressedError é a parte que torna o using seguro para uso em código com muitos erros.
Suporte atual: disponível, não “em breve”
O Gerenciamento Explícito de Recursos é uma proposta TC39 concluída e padronizada como parte do ES2026 — não um experimento em Stage 3 que requer transpilação em todo lugar. Em meados de 2026, o panorama é:
| Runtime | Status | Observações |
|---|---|---|
| Chrome / Edge / Opera | Disponível | using/await using desde o Chromium 134 (V8 13.8); o símbolo Symbol.dispose sozinho está presente desde o Chrome 125 |
| Node.js | Disponível | using/await using nativo no Node.js 24 (V8 13.6); símbolos desde a versão 18.18.0 |
| Firefox | Disponível | Desde o Firefox 141 (estável atual 152) |
| Safari | Parcial | O símbolo Symbol.dispose chegou ao Safari desktop 26.4; o Safari no iOS ainda não oferece suporte |
| TypeScript | Disponível | Sintaxe using desde a versão 5.2; estável atual 6.0 |
Uma armadilha merece destaque: um símbolo Symbol.dispose definido não significa que a engine analisa using. O símbolo frequentemente chega antes da declaração — o Node expôs os símbolos bem conhecidos ainda na linha v18 (18.18.0), mas só tornou a sintaxe de declaração nativa no Node 24, e o Chrome implementou o símbolo por volta da versão 125, mas não analisou using até a versão 134. Portanto, em uma engine mais antiga você pode referenciar Symbol.dispose e ainda assim obter um erro de sintaxe ou um TypeError “not disposable” — e uma tabela de suporte a Symbol.dispose (como a vinculada acima) está à frente do suporte real ao using. O suporte completo ao using é uma questão de versão da engine, não de polyfill.
Para o Safari e alvos mais antigos, utilize transpilação. O TypeScript requer a configuração do alvo de compilação para ES2022 ou inferior e a configuração de lib para incluir "esnext" ou "esnext.disposable" (o TypeScript oferece suporte à sintaxe desde a versão 5.2), e você pode fazer polyfill dos globais com core-js ou o pacote disposablestack.
O código de limpeza mecânico e propenso a erros que você tem escrito manualmente agora tem uma primitiva de linguagem: adicione [Symbol.dispose] ou [Symbol.asyncDispose] a qualquer coisa que precise de finalização, declare-a com using ou await using, e o runtime garante o descarte na ordem correta em todos os caminhos de saída. Comece envolvendo o recurso que você mais frequentemente esquece de fechar — uma conexão, um lock, um reader — e deixe o escopo fazer o resto.
Perguntas Frequentes
Qual é a diferença entre using e await using?
Uma declaração using chama o método síncrono Symbol.dispose do objeto quando o bloco é encerrado, enquanto await using chama Symbol.asyncDispose e o aguarda, para que a finalização que retorna uma promise seja concluída antes do fechamento do escopo. Use using quando a limpeza for síncrona e await using quando a finalização retornar uma promise. Como await using também aceita disposables síncronos simples, você pode utilizá-lo sempre que não tiver certeza de qual tipo de disposer um objeto possui.
Por que using lança um erro 'not disposable' no Node 18 mesmo que Symbol.dispose exista?
Um símbolo Symbol.dispose definido não significa que a engine consegue analisar a declaração using. O Node expôs os símbolos bem conhecidos Symbol.dispose e Symbol.asyncDispose desde a versão 18.18.0, mas a sintaxe completa das declarações using e await using só se tornou nativa no Node 24, que vem com o V8 13.6. Em versões mais antigas do Node, você pode referenciar o símbolo e ainda assim obter um erro de sintaxe ou um TypeError em identificadores nativos. O suporte completo ao using é uma questão de versão da engine, não de polyfill.
O Gerenciamento Explícito de Recursos substitui a coleta de lixo?
Não. A coleta de lixo recupera memória em seu próprio cronograma não determinístico e nunca fechou arquivos, liberou locks ou cancelou a inscrição de event listeners. O Gerenciamento Explícito de Recursos lida com esses recursos não relacionados à memória de forma determinística, executando sua limpeza no momento em que um escopo é encerrado. Os dois mecanismos abordam problemas diferentes. WeakRef e FinalizationRegistry são as ferramentas adjacentes à coleta de lixo para memória, enquanto using e await using fornecem finalização baseada em escopo de identificadores de arquivo, sockets, locks e conexões.
Como uso a palavra-chave using com uma biblioteca que não tem método dispose?
Anexe o símbolo você mesmo com Object.assign. Um cliente que expõe um método close ou end mas nenhum símbolo de descarte se torna descartável em uma única linha, atribuindo um método Symbol.asyncDispose assíncrono que chama o método close existente. Depois disso, você pode declarar o objeto com await using ou registrá-lo com uma chamada AsyncDisposableStack use. Se você posteriormente atualizar para uma versão da biblioteca que implemente o protocolo nativamente, a reatribuição aciona um erro lembrando-o de remover o shim.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k