A using deklaráció garantálja az erőforrás-felszabadítást – Node 22-n viszont csak félig igaz ez

A using és await using végre garantálja, hogy egy erőforrás felszabadítása lefusson – hiba, korai return esetén is. Node 24-en tökéletesen működik, Node 22 flag-elt verziója viszont némán elnyeli a dispose-hibákat.

Hányszor írtál már ilyet: megnyitsz egy fájlt, lekérsz egy DB-kapcsolatot a poolból, feloldasz egy lockot – aztán try/finally-be csomagolod, mert ha közben dobódik egy hiba, a takarítás simán lemarad. Most, hogy két-három ilyen erőforrás egymásba ágyazva várna felszabadításra, a kód recseg-ropog. A JavaScript ezt végre natívan megoldja: a using és await using deklaráció garantálja, hogy egy erőforrás felszabadítása lefut – blokk végén, korai return-nél, vagy dobott hibánál is. Kipróbáltam Node 22-n és Node 24-en is, és a kettő között meglepően nagy a különbség.

Három erőforrás-kártya egy kódblokkban, mindegyikből egy vonal fut egy zöld pipához, ami a garantált, sorrendben történő felszabadítást jelzi

Mi is ez pontosan

A TC39 Explicit Resource Management javaslat 2026 közepén elérte a Stage 4-et – a tc39/proposals lezárt javaslatok listája szerint várhatóan az ES2027 specifikáció része lesz. A lényege: ha egy változót using-gal deklarálsz, a hozzá tartozó [Symbol.dispose]() metódus automatikusan lefut, amikor a változó kikerül a scope-ból – blokk végén, korai visszatérésnél, vagy akár dobott kivétel esetén is. Az await using ugyanez aszinkron erőforrásokra, a [Symbol.asyncDispose]()-t hívja meg, és megvárja a Promise-t. Több using deklaráció fordított sorrendben (LIFO) dobódik el, pont úgy, mint egymásba ágyazott finally blokkoknál.

A MDN using dokumentációja jól összefoglalja a szintaktikai korlátokat is: script top-level-en és switch blokk tetején nem használható, viszont függvénytesten, modul top-leveljén és blokkokon belül igen. Nézzük meg élesben, Node.js-en (a példákat Node.js 24.19.0-n futtattam, flag nélkül – ez a fontos rész, mire ez alább kiderül).

import { open } from 'node:fs/promises';

async function withAwaitUsing() {
  await using fh = await open('/tmp/test-dispose.txt', 'w');
  await fh.writeFile('hello resource management\n');
  console.log('write done, handle still open here');
}

await withAwaitUsing();
console.log('after function: handle already closed automatically');

A FileHandle objektumnak Node.js-ben van beépített [Symbol.asyncDispose] metódusa – ez zárja le a fájlleírót. Nem kell külön try/finally, nem kell emlékezni a close()-ra korai return vagy hiba esetén sem. A kimenet:

write done, handle still open here
after function: handle already closed automatically

Ha kettő megy félre egyszerre: SuppressedError

Az igazán jó rész a hibakezelés. Mi történik, ha a blokk törzse dob egy hibát, és a takarítás (a dispose) is elhasal? A régi try/finally-nél az egyik hiba egyszerűen felülírja, elnyeli a másikat – ez klasszikus, nehezen debuggolható forrás. A specifikáció erre bevezette a SuppressedError-t: mindkét hiba megmarad, láncolva.

class Bad {
  [Symbol.dispose]() { throw new Error('dispose failed'); }
}

try {
  using r = new Bad();
  throw new Error('body failed');
} catch (e) {
  console.log(e instanceof SuppressedError); // true
  console.log(e.message);                    // An error was suppressed during disposal
  console.log(e.error.message);               // dispose failed  – az új hiba
  console.log(e.suppressed.message);          // body failed     – az eredeti, "elnyelt" hiba
}

Node 24-en pontosan ezt a kimenetet adja – semmi nem vész el, mindkét hiba elérhető marad az error/suppressed mezőkön keresztül.

DisposableStack: amikor előre nem tudod, mennyi erőforrás lesz

A using jól működik fix számú, előre ismert erőforrásra. De mi van, ha ciklusban vagy feltételesen szerzel erőforrásokat? Erre van a DisposableStack (és aszinkron párja, az AsyncDisposableStack): egy verem, amibe dinamikusan regisztrálhatsz felszabadítandó dolgokat, maga a stack pedig Symbol.dispose-olható, tehát using-gal kombinálható.

class Conn {
  close() { console.log('conn closed'); }
}

function doWork() {
  using stack = new DisposableStack();
  stack.defer(() => console.log('c1 disposed'));
  const conn = stack.adopt(new Conn(), (c) => c.close());
  console.log('doing work with', conn.constructor.name);
}

doWork();
// kimenet: doing work with Conn / conn closed / c1 disposed
// – fordított sorrendben, ahogy regisztrálva voltak

defer() egy tetszőleges callback-et regisztrál, adopt() pedig egy nem-disposable objektumhoz rendel egyedi takarító függvényt (pl. egy régi API-hoz, ami csak close()-t ismer, nem Symbol.dispose-t). Ez pont az a réteg, ami meglévő, nem „dispose-aware” library-k köré is használható.

A támogatottság: itt jön a csavar

A Symbol.dispose és Symbol.asyncDispose szimbólumok már Node.js 18.18 óta léteznek, de a using szintaxis maga sokkal később, csak a V8 megfelelő verziójával került be. A dokumentáció szerint az ERM (Explicit Resource Management) Node.js 24-től alapból (flag nélkül) engedélyezett, és a Symbol.dispose/Symbol.asyncDispose a 24.2.0-s kiadásban került ki a dokumentációban is jelzett experimental státuszból. A MDN böngésző-kompatibilitási adatbázisa szerint a DisposableStack konstruktor Node.js 24.0.0-tól, Chrome/Edge 134-től és Firefox 141-től elérhető – Safariban még nincs.

És itt jön a rész, amit érdemes tudni, mielőtt bárki bevezeti: Node 22-n (jelenleg is támogatott LTS-ág) a using csak a --js-explicit-resource-management V8-flaggel érhető el, és a flag saját leírása is „in progress / experimental”. Ez nem csak formalitás – lokálisan leteszteltem Node 22.23.2-n (V8 12.4), és:

  • A szinkron using a flaggel elfogadja a szintaxist és lefuttatja a dispose-t.
  • Az await using viszont ugyanezzel a flaggel SyntaxError-t dob – async függvényen belül és modul top-levelen is –, tehát a fájlkezelős példa fent Node 22-n el sem indul.
  • Ha a [Symbol.dispose]() hibát dob, az a flaggel némán elnyelődik – se kivétel, se SuppressedError, a program úgy folytatja, mintha semmi nem történt volna. Ugyanez Node 24-en unflagged helyesen szabályos Error-ként, illetve SuppressedError-ként jelenik meg, ahogy fentebb látszik.

Ez elég komoly különbség ahhoz, hogy ne bízz a flag-elt Node 22-es viselkedésben semmi olyanban, ami hibakezelésre épít. Ha most akarod bevezetni, vagy Node 24+ kell hozzá, vagy várj, vagy tesztelj a saját Node-verziódon, mielőtt bármi élesbe kerül belőle.

TypeScript-ben

A TypeScript 5.2 óta ismeri a using/await using szintaxist és típusait, "target": "es2022" és a "lib" listában "esnext.disposable" mellett. A release note maga hívja fel a figyelmet arra, hogy a legtöbb futtatási környezet natívan nem támogatja még – ha régebbi célra fordítasz, saját magadnak kell polyfillt adnod Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack és SuppressedError számára. A típusrendszer tehát előrébb van, mint a legtöbb futtatási cél – build- és target-konfiguráció nélkül könnyen abba a hibába lehet futni, hogy fordítási időben mindent helyesnek látsz, futásidőben pedig ReferenceError-t kapsz a régebbi Node-on vagy böngészőben.

Mikor éri meg

Ott, ahol eddig try/finally-t vagy egymásba ágyazott try blokkokat írtál: fájlleírók, DB-connectionök visszaadása a poolba, lockok feloldása, ideiglenes fájlok törlése, teszteknél a setup/teardown párosítása. A nyereség nem az, hogy kevesebbet gépelsz – inkább az, hogy a felszabadítás garantáltan lefut, és a hibák nem tűnnek el útközben. Ha viszont már van egy jól bejáratott try/finally-alapú megoldásod és nincs Node 24+ / friss böngésző célod, nincs sürgős ok az átírásra – ez inkább az új kódnál éri meg alapból így írni.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük