O Novo Superpoder do JavaScript: Gerenciamento Explícito de Recursos
A proposta de Gerenciamento Explícito de Recursos introduz uma abordagem determinística para gerenciar explicitamente o ciclo de vida de recursos como manipuladores de arquivos, conexões de rede e mais. Esta proposta traz as seguintes adições à linguagem: as declarações using
e await using
, que chamam automaticamente o método dispose quando um recurso sai do escopo; os símbolos [Symbol.dispose]()
e [Symbol.asyncDispose]()
para operações de limpeza; dois novos objetos globais DisposableStack
e AsyncDisposableStack
como contêineres para agregar recursos descartáveis; e SuppressedError
como um novo tipo de erro (contém tanto o erro que foi lançado mais recentemente, quanto o erro que foi suprimido) para lidar com o cenário onde um erro ocorre durante o descarte de um recurso, potencialmente mascarando um erro existente lançado pelo corpo ou pelo descarte de outro recurso. Essas adições permitem que os desenvolvedores escrevam códigos mais robustos, performáticos e mantíveis, fornecendo controle granular sobre o descarte de recursos.
Declarações using
e await using
O núcleo da proposta de Gerenciamento Explícito de Recursos está nas declarações using
e await using
. A declaração using
é projetada para recursos síncronos, garantindo que o método [Symbol.dispose]()
de um recurso descartável seja chamado quando o escopo em que ele foi declarado finalizar. Para recursos assíncronos, a declaração await using
funciona de forma semelhante, mas garante que o método [Symbol.asyncDispose]()
seja chamado e o resultado dessa chamada seja aguardado, permitindo operações de limpeza assíncronas. Essa distinção permite que os desenvolvedores gerenciem de forma confiável recursos síncronos e assíncronos, prevenindo vazamentos e melhorando a qualidade geral do código. As palavras-chave using
e await using
podem ser usadas dentro de chaves {}
(como blocos, loops for e corpos de função), e não podem ser usadas em níveis superiores.
Por exemplo, ao trabalhar com ReadableStreamDefaultReader
, é crucial chamar reader.releaseLock()
para desbloquear o fluxo e permitir que ele seja usado em outro lugar. No entanto, o tratamento de erros introduz um problema comum: se ocorrer um erro durante o processo de leitura e você esquecer de chamar releaseLock()
antes que o erro se propague, o fluxo permanecerá bloqueado. Vamos começar com um exemplo simples:
let responsePromise = null;
async function readFile(url) {
if (!responsePromise) {
// Só busca se ainda não tivermos uma promessa
responsePromise = fetch(url);
}
const response = await responsePromise;
if (!response.ok) {
throw new Error(`Erro HTTP! status: ${response.status}`);
}
const processedData = await processData(response);
// Faz algo com processedData
...
}
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Processa os dados e salva o resultado em processedData
...
// Um erro é lançado aqui!
}
}
// Como o erro é lançado antes desta linha, o fluxo permanece bloqueado.
reader.releaseLock();
return processedData;
}
readFile('https://example.com/largefile.dat');
Portanto, é crucial para os desenvolvedores utilizarem o bloco try...finally
ao usar fluxos e colocarem reader.releaseLock()
em finally
. Esse padrão garante que reader.releaseLock()
seja sempre chamado.
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;
try {
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Processa os dados e salva o resultado em processedData
...
// Um erro é lançado aqui!
}
}
} finally {
// O bloqueio do leitor no fluxo será sempre liberado.
reader.releaseLock();
}
return processedData;
}
readFile('https://example.com/largefile.dat');
Uma alternativa para escrever este código é criar um objeto descartável readerResource
, que contém o leitor (response.body.getReader()
) e o método [Symbol.dispose]()
que chama this.reader.releaseLock()
. A declaração using
garante que readerResource[Symbol.dispose]()
seja chamado quando o bloco de código for encerrado, e lembrar de chamar releaseLock
não é mais necessário porque a declaração using
cuida disso. A integração de [Symbol.dispose]
e [Symbol.asyncDispose]
em APIs web como streams pode acontecer no futuro, para que os desenvolvedores não precisem escrever o objeto wrapper manualmente.
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
// Envolva o leitor em um recurso descartável
using readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
},
};
const { reader } = readerResource;
let done = false;
let value;
let processedData;
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Processar os dados e salvar o resultado em processedData
...
// Um erro é lançado aqui!
}
}
return processedData;
}
// readerResource[Symbol.dispose]() é chamado automaticamente.
readFile('https://example.com/largefile.dat');
DisposableStack
e AsyncDisposableStack
Para facilitar ainda mais o gerenciamento de vários recursos descartáveis, a proposta introduz DisposableStack
e AsyncDisposableStack
. Essas estruturas baseadas em pilha permitem que os desenvolvedores agrupem e descartem vários recursos de maneira coordenada. Recursos são adicionados à pilha e, quando a pilha é descartada, de forma síncrona ou assíncrona, os recursos são descartados na ordem inversa em que foram adicionados, garantindo que quaisquer dependências entre eles sejam tratadas corretamente. Isso simplifica o processo de limpeza ao lidar com cenários complexos que envolvem vários recursos relacionados. 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. DisposableStack
e AsyncDisposableStack
têm [Symbol.dispose]()
e [Symbol.asyncDispose]()
, respectivamente, para que possam ser usados com as palavras-chave using
e await using
. Eles oferecem uma maneira robusta de gerenciar o descarte de vários recursos dentro de um escopo definido.
Vamos analisar cada método e ver um exemplo:
use(value)
adiciona um recurso ao topo da pilha.
{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Bloqueio do leitor liberado.');
},
};
using stack = new DisposableStack();
stack.use(readerResource);
}
// Bloqueio do leitor liberado.
adopt(value, onDispose)
adiciona um recurso não descartável e uma função de callback de descarte ao topo da pilha.
{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader => {
reader.releaseLock();
console.log('Bloqueio do leitor liberado.');
});
}
// Bloqueio do leitor liberado.
defer(onDispose)
adiciona uma função de callback de descarte ao topo da pilha. É útil para adicionar ações de limpeza que não têm um recurso associado.
{
using stack = new DisposableStack();
stack.defer(() => console.log("feito."));
}
// feito.
move()
move todos os recursos atualmente nesta pilha para uma nova DisposableStack
. Isso pode ser útil se você precisar transferir a propriedade de recursos para outra parte do seu código.
{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader => {
reader.releaseLock();
console.log('Bloqueio do leitor liberado.');
});
using newStack = stack.move();
}
// Aqui apenas a newStack existe e o recurso dentro dela será descartado.
// Bloqueio do leitor liberado.
dispose()
em DisposableStack e disposeAsync()
em AsyncDisposableStack descartam os recursos dentro deste objeto.
{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Bloqueio do leitor liberado.');
},
};
let stack = new DisposableStack();
stack.use(readerResource);
stack.dispose();
}
// Bloqueio do leitor liberado.
Disponibilidade
O Gerenciamento Explícito de Recursos está disponível no Chromium 134 e V8 v13.8.