Português

Domine o novo Gerenciamento Explícito de Recursos do JavaScript com `using` e `await using`. Aprenda a automatizar a limpeza, evitar vazamentos de recursos e escrever código mais limpo e robusto.

O Novo Superpoder do JavaScript: Um Mergulho Profundo no Gerenciamento Explícito de Recursos

No dinâmico mundo do desenvolvimento de software, gerenciar recursos de forma eficaz é um pilar para construir aplicações robustas, confiáveis e performáticas. Por décadas, desenvolvedores JavaScript confiaram em padrões manuais como try...catch...finally para garantir que recursos críticos — como manipuladores de arquivos, conexões de rede ou sessões de banco de dados — fossem liberados corretamente. Embora funcional, essa abordagem é frequentemente verbosa, propensa a erros e pode rapidamente se tornar complexa, um padrão às vezes chamado de "pirâmide da perdição" em cenários complexos.

Eis uma mudança de paradigma para a linguagem: Gerenciamento Explícito de Recursos (ERM). Finalizado no padrão ECMAScript 2024 (ES2024), este poderoso recurso, inspirado por construções semelhantes em linguagens como C#, Python e Java, introduz uma maneira declarativa e automatizada de lidar com a limpeza de recursos. Ao aproveitar as novas palavras-chave using e await using, o JavaScript agora oferece uma solução muito mais elegante e segura para um desafio atemporal da programação.

Este guia abrangente levará você a uma jornada pelo Gerenciamento Explícito de Recursos do JavaScript. Exploraremos os problemas que ele resolve, dissecaremos seus conceitos centrais, passaremos por exemplos práticos e descobriremos padrões avançados que o capacitarão a escrever um código mais limpo e resiliente, não importa em que parte do mundo você esteja desenvolvendo.

A Velha Guarda: Os Desafios da Limpeza Manual de Recursos

Antes de podermos apreciar a elegância do novo sistema, devemos primeiro entender os pontos problemáticos do antigo. O padrão clássico para o gerenciamento de recursos em JavaScript é o bloco try...finally.

A lógica é simples: você adquire um recurso no bloco try e o libera no bloco finally. O bloco finally garante a execução, quer o código no bloco try tenha sucesso, falhe ou retorne prematuramente.

Vamos considerar um cenário comum do lado do servidor: abrir um arquivo, escrever alguns dados nele e garantir que o arquivo seja fechado.

Exemplo: Uma Operação Simples de Arquivo com try...finally


const fs = require('fs/promises');

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Abrindo arquivo...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Escrevendo no arquivo...');
    await fileHandle.write(data);
    console.log('Dados escritos com sucesso.');
  } catch (error) {
    console.error('Ocorreu um erro durante o processamento do arquivo:', error);
  } finally {
    if (fileHandle) {
      console.log('Fechando arquivo...');
      await fileHandle.close();
    }
  }
}

Este código funciona, mas revela várias fraquezas:

Agora, imagine gerenciar múltiplos recursos, como uma conexão de banco de dados e um manipulador de arquivos. O código rapidamente se torna uma bagunça aninhada:


async function logQueryResultToFile(query, filePath) {
  let dbConnection;
  try {
    dbConnection = await getDbConnection();
    const result = await dbConnection.query(query);

    let fileHandle;
    try {
      fileHandle = await fs.open(filePath, 'w');
      await fileHandle.write(JSON.stringify(result));
    } finally {
      if (fileHandle) {
        await fileHandle.close();
      }
    }
  } finally {
    if (dbConnection) {
      await dbConnection.release();
    }
  }
}

Este aninhamento é difícil de manter e escalar. É um sinal claro de que uma abstração melhor é necessária. Este é precisamente o problema que o Gerenciamento Explícito de Recursos foi projetado para resolver.

Uma Mudança de Paradigma: Os Princípios do Gerenciamento Explícito de Recursos

O Gerenciamento Explícito de Recursos (ERM) introduz um contrato entre um objeto de recurso e o runtime do JavaScript. A ideia central é simples: um objeto pode declarar como deve ser limpo, e a linguagem fornece uma sintaxe para realizar essa limpeza automaticamente quando o objeto sai de escopo.

Isso é alcançado através de dois componentes principais:

  1. O Protocolo Descartável: Uma forma padrão para objetos definirem sua própria lógica de limpeza usando símbolos especiais: Symbol.dispose para limpeza síncrona e Symbol.asyncDispose para limpeza assíncrona.
  2. As Declarações `using` e `await using`: Novas palavras-chave que vinculam um recurso a um escopo de bloco. Quando o bloco é encerrado, o método de limpeza do recurso é invocado automaticamente.

Os Conceitos Centrais: `Symbol.dispose` e `Symbol.asyncDispose`

No coração do ERM estão dois novos Símbolos bem conhecidos. Um objeto que possui um método com um desses símbolos como chave é considerado um "recurso descartável".

Descarte Síncrono com `Symbol.dispose`

O símbolo Symbol.dispose especifica um método de limpeza síncrono. Isso é adequado para recursos onde a limpeza не requer operações assíncronas, como fechar um manipulador de arquivo sincronicamente ou liberar um bloqueio em memória.

Vamos criar um wrapper para um arquivo temporário que se limpa sozinho.


const fs = require('fs');
const path = require('path');

class TempFile {
  constructor(content) {
    this.path = path.join(__dirname, `temp_${Date.now()}.txt`);
    fs.writeFileSync(this.path, content);
    console.log(`Arquivo temporário criado: ${this.path}`);
  }

  // Este é o método descartável síncrono
  [Symbol.dispose]() {
    console.log(`Descartando arquivo temporário: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Arquivo deletado com sucesso.');
    } catch (error) {
      console.error(`Falha ao deletar arquivo: ${this.path}`, error);
      // É importante tratar erros dentro do dispose também!
    }
  }
}

Qualquer instância de `TempFile` agora é um recurso descartável. Ele tem um método com a chave `Symbol.dispose` que contém a lógica para deletar o arquivo do disco.

Descarte Assíncrono com `Symbol.asyncDispose`

Muitas operações de limpeza modernas são assíncronas. Fechar uma conexão de banco de dados pode envolver o envio de um comando `QUIT` pela rede, ou um cliente de fila de mensagens pode precisar descarregar seu buffer de saída. Para esses cenários, usamos `Symbol.asyncDispose`.

O método associado a `Symbol.asyncDispose` deve retornar uma `Promise` (ou ser uma função `async`).

Vamos modelar uma conexão de banco de dados simulada que precisa ser liberada de volta para um pool de forma assíncrona.


// Um pool de conexões de banco de dados simulado
const mockDbPool = {
  getConnection: () => {
    console.log('Conexão com o BD adquirida.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Executando consulta: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // Este é o método descartável assíncrono
  async [Symbol.asyncDispose]() {
    console.log('Liberando conexão com o BD de volta para o pool...');
    // Simula um atraso de rede para liberar a conexão
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('Conexão com o BD liberada.');
  }
}

Agora, qualquer instância de `MockDbConnection` é um recurso descartável assíncrono. Ele sabe como se liberar de forma assíncrona quando não for mais necessário.

A Nova Sintaxe: `using` e `await using` em Ação

Com nossas classes descartáveis definidas, podemos agora usar as novas palavras-chave para gerenciá-las automaticamente. Essas palavras-chave criam declarações com escopo de bloco, assim como `let` e `const`.

Limpeza Síncrona com `using`

A palavra-chave `using` é usada para recursos que implementam `Symbol.dispose`. Quando a execução do código sai do bloco onde a declaração `using` foi feita, o método `[Symbol.dispose]()` é chamado automaticamente.

Vamos usar nossa classe `TempFile`:


function processDataWithTempFile() {
  console.log('Entrando no bloco...');
  using tempFile = new TempFile('Estes são alguns dados importantes.');

  // Você pode trabalhar com tempFile aqui
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Lido do arquivo temporário: "${content}"`);

  // Nenhum código de limpeza é necessário aqui!
  console.log('...fazendo mais trabalho...');
} // <-- tempFile.[Symbol.dispose]() é chamado automaticamente bem aqui!

processDataWithTempFile();
console.log('O bloco foi encerrado.');

A saída seria:

Entrando no bloco...
Arquivo temporário criado: /caminho/para/temp_1678886400000.txt
Lido do arquivo temporário: "Estes são alguns dados importantes."
...fazendo mais trabalho...
Descartando arquivo temporário: /caminho/para/temp_1678886400000.txt
Arquivo deletado com sucesso.
O bloco foi encerrado.

Veja como isso é limpo! O ciclo de vida completo do recurso está contido dentro do bloco. Nós o declaramos, o usamos e esquecemos dele. A linguagem cuida da limpeza. Isso é uma melhoria enorme em legibilidade e segurança.

Gerenciando Múltiplos Recursos

Você pode ter múltiplas declarações `using` no mesmo bloco. Elas serão descartadas na ordem inversa de sua criação (um comportamento LIFO ou "semelhante a uma pilha").


{
  using resourceA = new MyDisposable('A'); // Criado primeiro
  using resourceB = new MyDisposable('B'); // Criado segundo
  console.log('Dentro do bloco, usando recursos...');
} // resourceB é descartado primeiro, depois resourceA

Limpeza Assíncrona com `await using`

A palavra-chave `await using` é a contraparte assíncrona do `using`. É usada para recursos que implementam `Symbol.asyncDispose`. Como a limpeza é assíncrona, esta palavra-chave só pode ser usada dentro de uma função `async` ou no nível superior de um módulo (se o await de nível superior for suportado).

Vamos usar nossa classe `MockDbConnection`:


async function performDatabaseOperation() {
  console.log('Entrando na função assíncrona...');
  await using db = mockDbPool.getConnection();

  await db.query('SELECT * FROM users');

  console.log('Operação de banco de dados completa.');
} // <-- await db.[Symbol.asyncDispose]() é chamado automaticamente aqui!

(async () => {
  await performDatabaseOperation();
  console.log('Função assíncrona foi concluída.');
})();

A saída demonstra a limpeza assíncrona:

Entrando na função assíncrona...
Conexão com o BD adquirida.
Executando consulta: SELECT * FROM users
Operação de banco de dados completa.
Liberando conexão com o BD de volta para o pool...
(aguarda 50ms)
Conexão com o BD liberada.
Função assíncrona foi concluída.

Assim como com `using`, a sintaxe `await using` lida com todo o ciclo de vida, mas ela `aguarda` corretamente o processo de limpeza assíncrono. Ela pode até lidar com recursos que são apenas descartáveis sincronicamente — ela simplesmente não os aguardará.

Padrões Avançados: `DisposableStack` e `AsyncDisposableStack`

Às vezes, o escopo de bloco simples do `using` não é flexível o suficiente. E se você precisar gerenciar um grupo de recursos com um tempo de vida que não está vinculado a um único bloco léxico? Ou se você estiver integrando com uma biblioteca mais antiga que não produz objetos com `Symbol.dispose`?

Para esses cenários, o JavaScript fornece duas classes auxiliares: `DisposableStack` e `AsyncDisposableStack`.

`DisposableStack`: O Gerenciador de Limpeza Flexível

Um `DisposableStack` é um objeto que gerencia uma coleção de operações de limpeza. Ele é, por si só, um recurso descartável, então você pode gerenciar todo o seu ciclo de vida com um bloco `using`.

Ele possui vários métodos úteis:

Exemplo: Gerenciamento Condicional de Recursos

Imagine uma função que abre um arquivo de log apenas se uma determinada condição for atendida, mas você quer que toda a limpeza aconteça em um único lugar no final.


function processWithConditionalLogging(shouldLog) {
  using stack = new DisposableStack();

  const db = stack.use(getDbConnection()); // Sempre usa o BD

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Adia a limpeza para o stream
    stack.defer(() => {
      console.log('Fechando o stream do arquivo de log...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- A pilha é descartada, chamando todas as funções de limpeza registradas em ordem LIFO.

`AsyncDisposableStack`: Para o Mundo Assíncrono

Como você pode imaginar, `AsyncDisposableStack` é a versão assíncrona. Ele pode gerenciar tanto descartáveis síncronos quanto assíncronos. Seu método de limpeza principal é `.disposeAsync()`, que retorna uma `Promise` que resolve quando todas as operações de limpeza assíncronas são concluídas.

Exemplo: Gerenciando uma Mistura de Recursos

Vamos criar um manipulador de requisições de servidor web que precisa de uma conexão de banco de dados (limpeza assíncrona) e um arquivo temporário (limpeza síncrona).


async function handleRequest() {
  await using stack = new AsyncDisposableStack();

  // Gerencia um recurso descartável assíncrono
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Gerencia um recurso descartável síncrono
  const tempFile = stack.use(new TempFile('dados da requisição'));

  // Adota um recurso de uma API antiga
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Processando requisição...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() é chamado. Ele aguardará corretamente a limpeza assíncrona.

O `AsyncDisposableStack` é uma ferramenta poderosa para orquestrar lógicas complexas de configuração e desmontagem de forma limpa e previsível.

Tratamento de Erros Robusto com `SuppressedError`

Uma das melhorias mais sutis, mas significativas, do ERM é como ele lida com erros. O que acontece se um erro for lançado dentro do bloco `using` e *outro* erro for lançado durante o descarte automático subsequente?

No antigo mundo do `try...finally`, o erro do bloco `finally` normalmente sobrescreveria ou "suprimiria" o erro original e mais importante do bloco `try`. Isso muitas vezes tornava a depuração incrivelmente difícil.

O ERM resolve isso com um novo tipo de erro global: `SuppressedError`. Se um erro ocorrer durante o descarte enquanto outro erro já está se propagando, o erro de descarte é "suprimido". O erro original é lançado, mas agora ele tem uma propriedade `suppressed` contendo o erro de descarte.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Erro durante o descarte!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Erro durante a operação!');
} catch (e) {
  console.log(`Erro capturado: ${e.message}`); // Erro durante a operação!
  if (e.suppressed) {
    console.log(`Erro suprimido: ${e.suppressed.message}`); // Erro durante o descarte!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Este comportamento garante que você nunca perca o contexto da falha original, levando a sistemas muito mais robustos e depuráveis.

Casos de Uso Práticos em Todo o Ecossistema JavaScript

As aplicações do Gerenciamento Explícito de Recursos são vastas e relevantes para desenvolvedores em todo o mundo, quer estejam trabalhando no back-end, front-end ou em testes.

Suporte de Navegadores e Runtimes

Sendo um recurso moderno, é importante saber onde você pode usar o Gerenciamento Explícito de Recursos. A partir do final de 2023 / início de 2024, o suporte é generalizado nas versões mais recentes dos principais ambientes JavaScript:

Para ambientes mais antigos, você precisará contar com transpiladores como o Babel com os plugins apropriados para transformar a sintaxe `using` e fornecer polyfills para os símbolos e classes de pilha necessários.

Conclusão: Uma Nova Era de Segurança e Clareza

O Gerenciamento Explícito de Recursos do JavaScript é mais do que apenas açúcar sintático; é uma melhoria fundamental na linguagem que promove segurança, clareza e manutenibilidade. Ao automatizar o processo tedioso e propenso a erros de limpeza de recursos, ele libera os desenvolvedores para se concentrarem em sua lógica de negócios principal.

Os pontos principais são:

Ao iniciar novos projetos ou refatorar código existente, considere adotar este novo e poderoso padrão. Ele tornará seu JavaScript mais limpo, suas aplicações mais confiáveis e sua vida como desenvolvedor um pouco mais fácil. É um padrão verdadeiramente global para escrever JavaScript moderno e profissional.