Norsk

Mestre JavaScripts nye Explicit Resource Management med `using` og `await using`. Lær å automatisere opprydding, forhindre ressurslekkasjer og skrive renere, mer robust kode.

JavaScripts nye superkraft: Et dypdykk i Explicit Resource Management

I den dynamiske verdenen av programvareutvikling er effektiv ressursstyring en hjørnestein for å bygge robuste, pålitelige og ytelsessterke applikasjoner. I tiår har JavaScript-utviklere stolt på manuelle mønstre som try...catch...finally for å sikre at kritiske ressurser – som filhåndtak, nettverkstilkoblinger eller databaseøkter – frigjøres korrekt. Selv om denne tilnærmingen er funksjonell, er den ofte omstendelig, feilutsatt og kan raskt bli uhåndterlig, et mønster som noen ganger refereres til som «pyramide of doom» i komplekse scenarier.

Her kommer et paradigmeskifte for språket: Explicit Resource Management (ERM). Denne kraftige funksjonen, som ble ferdigstilt i ECMAScript 2024 (ES2024)-standarden og er inspirert av lignende konstruksjoner i språk som C#, Python og Java, introduserer en deklarativ og automatisert måte å håndtere ressursopprydding på. Ved å utnytte de nye nøkkelordene using og await using, gir JavaScript nå en langt mer elegant og sikrere løsning på en tidløs programmeringsutfordring.

Denne omfattende guiden vil ta deg med på en reise gjennom JavaScripts Explicit Resource Management. Vi vil utforske problemene den løser, dissekere kjernekonseptene, gå gjennom praktiske eksempler og avdekke avanserte mønstre som vil gi deg muligheten til å skrive renere og mer motstandsdyktig kode, uansett hvor i verden du utvikler.

Den gamle garden: Utfordringene med manuell ressursopprydding

Før vi kan sette pris på elegansen i det nye systemet, må vi først forstå smertepunktene i det gamle. Det klassiske mønsteret for ressursstyring i JavaScript er try...finally-blokken.

Logikken er enkel: du anskaffer en ressurs i try-blokken, og du frigjør den i finally-blokken. finally-blokken garanterer utførelse, enten koden i try-blokken lykkes, mislykkes eller returnerer for tidlig.

La oss vurdere et vanlig scenario på serversiden: å åpne en fil, skrive data til den, og deretter sørge for at filen lukkes.

Eksempel: En enkel filoperasjon med try...finally


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

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Opening file...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Writing to file...');
    await fileHandle.write(data);
    console.log('Data written successfully.');
  } catch (error) {
    console.error('An error occurred during file processing:', error);
  } finally {
    if (fileHandle) {
      console.log('Closing file...');
      await fileHandle.close();
    }
  }
}

Denne koden fungerer, men den avslører flere svakheter:

Tenk deg nå å administrere flere ressurser, som en databasetilkobling og et filhåndtak. Koden blir raskt et rotete, nøstet kaos:


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();
    }
  }
}

Denne nøstingen er vanskelig å vedlikeholde og skalere. Det er et tydelig signal om at en bedre abstraksjon er nødvendig. Dette er nøyaktig det problemet Explicit Resource Management ble designet for å løse.

Et paradigmeskifte: Prinsippene bak Explicit Resource Management

Explicit Resource Management (ERM) introduserer en kontrakt mellom et ressursobjekt og JavaScript-kjøremiljøet. Kjerneideen er enkel: et objekt kan deklarere hvordan det skal ryddes opp, og språket tilbyr syntaks for automatisk å utføre den oppryddingen når objektet går ut av scope.

Dette oppnås gjennom to hovedkomponenter:

  1. The Disposable Protocol: En standard måte for objekter å definere sin egen oppryddingslogikk ved hjelp av spesielle symboler: Symbol.dispose for synkron opprydding og Symbol.asyncDispose for asynkron opprydding.
  2. using- og await using-deklarasjonene: Nye nøkkelord som binder en ressurs til et blokk-scope. Når blokken forlates, blir ressursens oppryddingsmetode automatisk kalt.

Kjernekonseptene: `Symbol.dispose` og `Symbol.asyncDispose`

I hjertet av ERM er to nye velkjente symboler. Et objekt som har en metode med ett av disse symbolene som nøkkel, anses som en «disposable ressurs».

Synkron frigjøring med `Symbol.dispose`

Symbol.dispose-symbolet spesifiserer en synkron oppryddingsmetode. Dette passer for ressurser der opprydding ikke krever asynkrone operasjoner, som å lukke et filhåndtak synkront eller frigjøre en minnebasert lås.

La oss lage en omslutning (wrapper) for en midlertidig fil som rydder opp etter seg selv.


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(`Created temp file: ${this.path}`);
  }

  // Dette er den synkrone disposable-metoden
  [Symbol.dispose]() {
    console.log(`Disposing temp file: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('File deleted successfully.');
    } catch (error) {
      console.error(`Failed to delete file: ${this.path}`, error);
      // Det er viktig å håndtere feil også inne i dispose!
    }
  }
}

Enhver instans av `TempFile` er nå en «disposable» ressurs. Den har en metode med `Symbol.dispose` som nøkkel, som inneholder logikken for å slette filen fra disken.

Asynkron frigjøring med `Symbol.asyncDispose`

Mange moderne oppryddingsoperasjoner er asynkrone. Å lukke en databasetilkobling kan innebære å sende en QUIT-kommando over nettverket, eller en meldingskø-klient kan trenge å tømme sin utgående buffer. For disse scenarioene bruker vi Symbol.asyncDispose.

Metoden tilknyttet Symbol.asyncDispose må returnere et Promise (eller være en async-funksjon).

La oss modellere en fiktiv databasetilkobling som må frigjøres tilbake til en pool asynkront.


// En fiktiv database-pool
const mockDbPool = {
  getConnection: () => {
    console.log('DB connection acquired.');
    return new MockDbConnection();
  }
};

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

  // Dette er den asynkrone disposable-metoden
  async [Symbol.asyncDispose]() {
    console.log('Releasing DB connection back to the pool...');
    // Simuler en nettverksforsinkelse for å frigjøre tilkoblingen
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB connection released.');
  }
}

Nå er enhver `MockDbConnection`-instans en asynkron disposable ressurs. Den vet hvordan den skal frigjøre seg selv asynkront når den ikke lenger er nødvendig.

Den nye syntaksen: `using` og `await using` i praksis

Nå som våre «disposable» klasser er definert, kan vi bruke de nye nøkkelordene til å administrere dem automatisk. Disse nøkkelordene oppretter blokk-scopede deklarasjoner, akkurat som `let` og `const`.

Synkron opprydding med `using`

Nøkkelordet `using` brukes for ressurser som implementerer `Symbol.dispose`. Når kjøringen forlater blokken der `using`-deklarasjonen ble gjort, kalles `[Symbol.dispose]()`-metoden automatisk.

La oss bruke `TempFile`-klassen vår:


function processDataWithTempFile() {
  console.log('Entering block...');
  using tempFile = new TempFile('This is some important data.');

  // Du kan jobbe med tempFile her
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Read from temp file: "${content}"`);

  // Ingen oppryddingskode er nødvendig her!
  console.log('...doing more work...');
} // <-- tempFile.[Symbol.dispose]() kalles automatisk akkurat her!

processDataWithTempFile();
console.log('Block has been exited.');

Resultatet ville vært:

Entering block...
Created temp file: /path/to/temp_1678886400000.txt
Read from temp file: "This is some important data."
...doing more work...
Disposing temp file: /path/to/temp_1678886400000.txt
File deleted successfully.
Block has been exited.

Se hvor rent det er! Ressursens hele livssyklus er inneholdt i blokken. Vi deklarerer den, vi bruker den, og vi glemmer den. Språket håndterer oppryddingen. Dette er en massiv forbedring i lesbarhet og sikkerhet.

Håndtering av flere ressurser

Du kan ha flere `using`-deklarasjoner i samme blokk. De vil bli frigjort i motsatt rekkefølge av hvordan de ble opprettet (en LIFO eller «stack-lignende» oppførsel).


{
  using resourceA = new MyDisposable('A'); // Opprettet først
  using resourceB = new MyDisposable('B'); // Opprettet som nummer to
  console.log('Inside block, using resources...');
} // resourceB frigjøres først, deretter resourceA

Asynkron opprydding med `await using`

Nøkkelordet `await using` er den asynkrone motparten til `using`. Det brukes for ressurser som implementerer `Symbol.asyncDispose`. Siden oppryddingen er asynkron, kan dette nøkkelordet kun brukes inne i en `async`-funksjon eller på toppnivå i en modul (hvis top-level await støttes).

La oss bruke `MockDbConnection`-klassen vår:


async function performDatabaseOperation() {
  console.log('Entering async function...');
  await using db = mockDbPool.getConnection();

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

  console.log('Database operation complete.');
} // <-- await db.[Symbol.asyncDispose]() kalles automatisk her!

(async () => {
  await performDatabaseOperation();
  console.log('Async function has completed.');
})();

Resultatet demonstrerer den asynkrone oppryddingen:

Entering async function...
DB connection acquired.
Executing query: SELECT * FROM users
Database operation complete.
Releasing DB connection back to the pool...
(venter 50ms)
DB connection released.
Async function has completed.

Akkurat som med `using`, håndterer `await using`-syntaksen hele livssyklusen, men den venter korrekt på (`awaits`) den asynkrone oppryddingsprosessen. Den kan til og med håndtere ressurser som kun er synkront «disposable» – den vil bare ikke vente på dem.

Avanserte mønstre: `DisposableStack` og `AsyncDisposableStack`

Noen ganger er den enkle blokk-scopingen til `using` ikke fleksibel nok. Hva om du trenger å administrere en gruppe ressurser med en levetid som ikke er knyttet til en enkelt leksikalsk blokk? Eller hva om du integrerer med et eldre bibliotek som ikke produserer objekter med `Symbol.dispose`?

For disse scenarioene tilbyr JavaScript to hjelpeklasser: `DisposableStack` og `AsyncDisposableStack`.

`DisposableStack`: Den fleksible oppryddingsbehandleren

En `DisposableStack` er et objekt som administrerer en samling av oppryddingsoperasjoner. Den er i seg selv en «disposable» ressurs, så du kan administrere hele dens levetid med en `using`-blokk.

Den har flere nyttige metoder:

Eksempel: Betinget ressursstyring

Tenk deg en funksjon som åpner en loggfil bare hvis en bestemt betingelse er oppfylt, men du vil at all opprydding skal skje på ett sted på slutten.


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

  const db = stack.use(getDbConnection()); // Bruk alltid databasen

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Utsett oppryddingen for streamen
    stack.defer(() => {
      console.log('Closing log file stream...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Stacken frigjøres, og kaller alle registrerte oppryddingsfunksjoner i LIFO-rekkefølge.

`AsyncDisposableStack`: For den asynkrone verden

Som du kanskje gjetter, er `AsyncDisposableStack` den asynkrone versjonen. Den kan administrere både synkrone og asynkrone «disposables». Dens primære oppryddingsmetode er `.disposeAsync()`, som returnerer et `Promise` som løses når alle asynkrone oppryddingsoperasjoner er fullført.

Eksempel: Håndtering av en blanding av ressurser

La oss lage en behandler for en webserver-forespørsel som trenger en databasetilkobling (asynkron opprydding) og en midlertidig fil (synkron opprydding).


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

  // Administrer en asynkron disposable ressurs
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Administrer en synkron disposable ressurs
  const tempFile = stack.use(new TempFile('request data'));

  // Adopter en ressurs fra et gammelt API
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Processing request...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() kalles. Den vil vente korrekt på asynkron opprydding.

`AsyncDisposableStack` er et kraftig verktøy for å orkestrere kompleks oppsett- og nedrivingslogikk på en ren og forutsigbar måte.

Robust feilhåndtering med `SuppressedError`

En av de mest subtile, men betydningsfulle forbedringene med ERM, er hvordan den håndterer feil. Hva skjer hvis en feil kastes inne i `using`-blokken, og *en annen* feil kastes under den påfølgende automatiske frigjøringen?

I den gamle `try...finally`-verdenen ville feilen fra `finally`-blokken vanligvis overskrive eller «undertrykke» den opprinnelige, viktigere feilen fra `try`-blokken. Dette gjorde ofte feilsøking utrolig vanskelig.

ERM løser dette med en ny global feiltype: `SuppressedError`. Hvis en feil oppstår under frigjøring mens en annen feil allerede propagerer, blir frigjøringsfeilen «undertrykt». Den opprinnelige feilen kastes, men den har nå en `suppressed`-egenskap som inneholder frigjøringsfeilen.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Error during disposal!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Error during operation!');
} catch (e) {
  console.log(`Caught error: ${e.message}`); // Error during operation!
  if (e.suppressed) {
    console.log(`Suppressed error: ${e.suppressed.message}`); // Error during disposal!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Denne oppførselen sikrer at du aldri mister konteksten til den opprinnelige feilen, noe som fører til mye mer robuste og feilsøkbare systemer.

Praktiske bruksområder i hele JavaScript-økosystemet

Anvendelsene av Explicit Resource Management er enorme og relevante for utviklere over hele verden, enten de jobber på back-end, front-end eller med testing.

Støtte i nettlesere og kjøremiljøer

Som en moderne funksjon er det viktig å vite hvor du kan bruke Explicit Resource Management. Fra slutten av 2023 / begynnelsen av 2024 er støtten utbredt i de nyeste versjonene av store JavaScript-miljøer:

For eldre miljøer må du stole på transpilere som Babel med de riktige pluginene for å transformere `using`-syntaksen og polyfille de nødvendige symbolene og stack-klassene.

Konklusjon: En ny æra av sikkerhet og klarhet

JavaScript's Explicit Resource Management er mer enn bare syntaktisk sukker; det er en fundamental forbedring av språket som fremmer sikkerhet, klarhet og vedlikeholdbarhet. Ved å automatisere den kjedelige og feilutsatte prosessen med ressursopprydding, frigjør det utviklere til å fokusere på sin primære forretningslogikk.

De viktigste poengene er:

Når du starter nye prosjekter eller refaktorerer eksisterende kode, bør du vurdere å ta i bruk dette kraftige nye mønsteret. Det vil gjøre din JavaScript renere, applikasjonene dine mer pålitelige, og livet ditt som utvikler bare litt enklere. Det er en virkelig global standard for å skrive moderne, profesjonell JavaScript.