Wie JavaScript endlich gelernt hat, nach sich selbst aufzuräumen
Explicit Resource Management in JavaScript mit using, await using, DisposableStack und SuppressedError für deterministische Bereinigung von Dateien, Locks, Sockets und Verbindungen.
JavaScripts neue Funktion „Explicit Resource Management” – die Deklarationen using und await using, die durch Symbol.dispose und Symbol.asyncDispose unterstützt werden – gibt der Sprache eine deterministische, geltungsbereichsbasierte Bereinigung von Nicht-Speicher-Ressourcen wie Datei-Handles, Sockets, Locks und Datenbankverbindungen. Dies ist keine Garbage Collection. Die Garbage Collection gibt Speicher nach einem eigenen, nicht-deterministischen Zeitplan frei und hat niemals Ihre Dateien geschlossen, Ihre Locks freigegeben oder Ihre Event-Listener abgemeldet. Genau das sind die Ressourcen, die using bereinigt – zuverlässig und vorhersehbar, in dem Moment, in dem ein Geltungsbereich verlassen wird.
Wenn Sie bisher try/finally-Blöcke von Hand geschrieben haben, um Verbindungen zu schließen und Locks freizugeben, ersetzt dieses Feature diesen Boilerplate-Code durch ein einziges Schlüsselwort. Dieser Artikel erklärt, was using leistet, wie die synchrone/asynchrone Aufteilung funktioniert, wie Sie eigene Disposables schreiben, wie Sie mehrere davon mit DisposableStack koordinieren, wie Sie Bibliotheken nachrüsten, die es noch nicht unterstützen, und wie Bereinigungsfehler durch den neuen SuppressedError sichtbar werden. (WeakRef und FinalizationRegistry sind die GC-nahen Werkzeuge für den Speicher; dies ist ein völlig anderer Mechanismus.)
Die wichtigsten Erkenntnisse
- Eine
using-Deklaration ruft die Methode[Symbol.dispose]()eines Objekts auf, wenn der umschließende Block verlassen wird;await usingruft[Symbol.asyncDispose]()auf und wartet darauf, sodass promise-basiertes Teardown abgeschlossen wird, bevor der Geltungsbereich geschlossen wird. - Wenn sich mehrere Ressourcen denselben Geltungsbereich teilen, werden sie in umgekehrter Deklarationsreihenfolge bereinigt, sodass eine abhängige Ressource vor der Ressource, von der sie abhängt, abgebaut wird.
- Explicit Resource Management ist ein abgeschlossener TC39-Vorschlag, der in ES2026 standardisiert wurde und nativ in Chrome 134 (V8 13.8), Firefox 141 und Node.js 24 (V8 13.6) verfügbar ist; Safari holt noch auf – das Symbol
Symbol.disposeist in Desktop-Safari 26.4 angekommen, aber noch nicht in Safari für iOS. DisposableStack.prototype.move()überträgt registrierte Ressourcen in einen neuen Stack und markiert den ursprünglichen Stack als bereinigt, ohne dabei etwas zu entsorgen – das Muster für die sichere Übergabe halb aufgebauter Ressourcen aus einem Konstruktor heraus.- Wenn eine Bereinigung fehlschlägt, während Ihr Code bereits einen Fehler wirft, kapselt JavaScript beide in einem
SuppressedError, sodass weder der ursprüngliche Fehler noch der Bereinigungsfehler verloren geht.
Das Bereinigungsproblem, das try/finally nie wirklich gelöst hat
Jede Ressource, die Sie öffnen – ein Datei-Handle, ein Stream-Reader, eine Datenbankverbindung, ein Lock – muss wieder geschlossen werden, und das einzige sprachseitige Werkzeug, das dies garantierte, war try/finally. Es funktioniert, ist aber ausführlich und skaliert schlecht. Betrachten Sie einen Web-Streams-Reader: Tritt während des Lesevorgangs ein Fehler auf und Sie vergessen, releaseLock() aufzurufen, bevor der Fehler weitergegeben wird, bleibt der Stream gesperrt. Die Lösung besteht darin, die Leseschleife in einen try-Block einzuwickeln und reader.releaseLock() in finally zu platzieren, damit der Lock immer freigegeben wird.
Dieses Muster ist für eine Ressource in Ordnung. Bei zwei oder drei voneinander abhängigen Ressourcen entstehen verschachtelte finally-Blöcke, bei denen die Reihenfolge wichtig ist und ein Fehler innerhalb einer Bereinigung die anderen überspringen kann. Der mechanische Teil – „dieses Ding braucht ein Teardown, führe es beim Verlassen aus, in der richtigen Reihenfolge, auch auf dem Fehlerpfad” – ist genau das, was die Sprache jetzt für Sie übernimmt.
Wie das Schlüsselwort using funktioniert
Discover how at OpenReplay.com.
Das Schlüsselwort using deklariert eine blockbegrenzte, nicht neu zuweisbare Bindung (ähnlich wie const), deren Wert null, undefined oder ein Objekt mit einer [Symbol.dispose]()-Methode sein muss; wenn die Variable den Geltungsbereich verlässt, wird diese Methode automatisch ausgeführt. Verwenden Sie using für synchrone Bereinigung. Verwenden Sie await using, wenn das Teardown ein Promise zurückgibt – es deklariert blockbegrenzte Variablen, die asynchron bereinigt werden; der Wert muss eine [Symbol.asyncDispose]()- oder [Symbol.dispose]()-Methode besitzen, und diese Methode wird aufgerufen und abgewartet, wenn die Variable den Geltungsbereich verlässt. Da await using auch einfache synchrone Disposables akzeptiert, können Sie es immer dann verwenden, wenn Sie nicht wissen, welche Art Sie haben.
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]() wird hier auf jedem Ausstiegspfad aufgerufen und abgewartet
}
Node.js-FileHandle-Objekte implementieren das asynchrone Disposable-Protokoll bereits, sodass dies ohne manuellen Wrapper funktioniert. Beachten Sie die zwei await-Operationen: await fs.open() wartet auf die Akquise und entpackt das Promise in ein FileHandle, während await using auf die Bereinigung wartet, wenn die Variable den Geltungsbereich verlässt.
Die Reihenfolgeregel ist am wichtigsten, wenn Ressourcen voneinander abhängen. Enthält ein Geltungsbereich mehrere using- oder await using-Deklarationen, werden alle Disposer der Reihe nach in umgekehrter Deklarationsreihenfolge ausgeführt, unabhängig vom Deklarationstyp, und alle werden garantiert ausgeführt – ähnlich wie ein finally-Block.
await using db = await openConnection(); // wird als zweites bereinigt
await using tx = await db.beginTransaction(); // wird als erstes bereinigt
// tx wird abgebaut, bevor db bereinigt wird, sodass die Verbindung noch aktiv ist,
// wenn die Transaktion abgeschlossen wird
Zur Geltungsbereichsregel: using ist in jedem Block, Funktionskörper, statischen Initialisierungsblock, for-Header und auf der obersten Ebene eines Moduls gültig – aber nicht auf der obersten Ebene eines Scripts, da Script-Geltungsbereiche fortbestehen und der Disposer niemals ausgelöst würde. await using auf der obersten Ebene ist nur in Modulen möglich, da es von Top-Level-await abhängt. Die spezifikationskonformen Regeln sind in der MDN-Referenz zu using dokumentiert.
Eigene Disposables schreiben
Jedes Objekt wird durch die Implementierung von [Symbol.dispose]() für synchrone oder [Symbol.asyncDispose]() für asynchrone Bereinigung zu einem Disposable – es gibt keine Basisklasse oder einen Registrierungsschritt. Das bekannte Symbol ist der Vertrag: Ein Objekt ist disposable, wenn es eine [Symbol.dispose]()-Methode besitzt, und die using-Deklaration sucht dieses Symbol beim Initialisierer nach der Methode, die aufgerufen wird, wenn die Variable den Geltungsbereich verlässt.
Hier ist ein Datenbankverbindungs-Wrapper, der im weiteren Verlauf dieses Artikels verwendet wird:
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]() wird hier ausgeführt – der Client wird auf jedem Pfad freigegeben
}
Ein synchroner Disposer folgt derselben Form mit [Symbol.dispose]() anstelle davon. Ein synchroner Disposer sollte kein Promise zurückgeben, da von [Symbol.dispose]() zurückgegebene Promises von await using nicht abgewartet werden; für asynchrone Disposables verwenden Sie Symbol.asyncDispose.
Ressourcen mit DisposableStack koordinieren
Wenn ein einzelnes Objekt mehrere Ressourcen besitzt – oder wenn Sie Ressourcen in einer Schleife akquirieren – sammeln DisposableStack und AsyncDisposableStack diese ein und bereinigen die gesamte Gruppe auf einmal, in umgekehrter Reihenfolge. Beide Strukturen bieten Methoden wie use(), adopt() und defer(), um Ressourcen oder Bereinigungsaktionen hinzuzufügen, sowie eine dispose()- oder asyncDispose()-Methode, um die Bereinigung auszulösen. Sie selbst tragen [Symbol.dispose]() / [Symbol.asyncDispose](), sodass sie mit using und await using funktionieren.
Die drei Registrierungsmethoden decken unterschiedliche Formen ab:
| Methode | Registriert | Verwenden wenn |
|---|---|---|
use(resource) | Eine Ressource, die bereits ein Dispose-Symbol besitzt | Das Objekt ist selbst disposable |
adopt(value, onDispose) | Einen nicht-disposablen Wert plus einen Bereinigungsrückruf | Die Ressource hat eine close/abort-artige Methode, aber kein Symbol |
defer(onDispose) | Einen eigenständigen Bereinigungsrückruf, keine Ressource | Sie müssen beliebiges Teardown ausführen (z. B. clearInterval) |
function startWorker() {
using stack = new DisposableStack();
const handle = setInterval(poll, 5000);
stack.defer(() => clearInterval(handle));
stack.adopt(openSocket(), (s) => s.close());
// beide Bereinigungen werden in umgekehrter Reihenfolge ausgeführt, wenn der Block verlassen wird
}
Die nicht offensichtliche Stärke ist move(), das ein reales Konstruktionssicherheitsproblem löst. Manchmal reicht der Funktionsgeltungsbereich nicht aus – eine Klasse oder ein Objekt besitzt mehrere Ressourcen, die wie using gruppiert werden sollten, aber als Klassenfeld oder Closure – genau dafür sind DisposableStack und AsyncDisposableStack gedacht. DisposableStack.prototype.move() überträgt jede registrierte Ressource in einen neuen Stack und markiert den ursprünglichen als bereinigt, ohne die Ressourcen zu entsorgen, was es Ihnen ermöglicht, Ressourcen lokal aufzubauen und sie nur dann weiterzugeben, wenn die Konstruktion vollständig erfolgreich war:
function openResources() {
using cleanup = new DisposableStack();
const a = cleanup.use(openA());
const b = cleanup.use(openB()); // wenn dies einen Fehler wirft, wird `a` automatisch bereinigt
const moved = cleanup.move(); // Erfolg: Eigentümerschaft wird übertragen, nichts wird bereinigt
return moved; // der Aufrufer ist nun für die Bereinigung beider verantwortlich
}
Wenn openB() einen Fehler wirft, verlässt der Block den Geltungsbereich und cleanup bereinigt a – kein Leak. Wenn alles erfolgreich ist, übergibt move() die Ressourcen intakt an den Aufrufer. Die move()-Semantik ist im Explicit-Resource-Management-Artikel von V8 dokumentiert.
Bibliotheken nachrüsten, die Disposal noch nicht unterstützen
Sie müssen nicht warten, bis eine Bibliothek Symbol.dispose unterstützt, um es mit using zu verwenden – hängen Sie das Symbol selbst mit Object.assign an. Ein Bibliotheks-Client, der eine close()- oder end()-Methode bereitstellt, aber kein Dispose-Symbol besitzt, wird in einer Zeile zu einem Disposable. Das Zuweisen einer Symbol.asyncDispose-Methode an den Client bedeutet, dass Sie ihn in await using-Deklarationen und mit AsyncDisposableStack#use() verwenden können. Wenn Sie später auf eine Version upgraden, die das Protokoll selbst implementiert, erhalten Sie einen Fehler, der Sie daran erinnert, den Shim zu entfernen.
const client = await MongoClient.connect(url);
Object.assign(client, {
async [Symbol.asyncDispose]() {
await client.close();
},
});
async function run() {
await using db = client; // wird nun sauber bereinigt
// ...
}
Der Object.assign-Shim ist die Brücke für das aktuelle Ökosystem: Die meisten Drittanbieter-Clients haben noch keine Dispose-Symbole implementiert, und damit können Sie sie noch heute einbinden.
Wo dies im Frontend und in Node hilft
Auf der Client-Seite sind die Ressourcen, die Leaks verursachen, Event-Listener, IntersectionObserver/ResizeObserver-Instanzen, navigator.locks, gesperrte Web Streams und IndexedDB-Transaktionen – alles Dinge, die einen expliziten Freigabeschritt haben, der auf einem Fehlerpfad leicht vergessen werden kann. Das Einwickeln in ein Disposable bindet ihre Lebensdauer an einen Geltungsbereich. Das kanonische Beispiel des V8-Teams ist ein Stream-Reader: Eine using-Deklaration über ein Objekt, dessen [Symbol.dispose]() reader.releaseLock() aufruft, macht es überflüssig, sich überhaupt an die Freigabe zu erinnern.
Verwaiste Observer und nicht freigegebene Locks sind genau die Leaks, die bei einem schnellen lokalen Test unsichtbar bleiben. Ein verwaister Observer oder ein nicht freigegebener Lock kostet nichts auf einer Seite, die man alle paar Sekunden neu lädt, aber über eine lange Single-Page-App-Sitzung häufen sie sich zu einem allmählichen Speicherwachstum und Interaktionsverzögerungen an – die schleichende Degradation, die eine vollständige Session-Replay-Aufzeichnung sichtbar macht, wenn ein einmaliger Reload das Problem nie reproduziert. Das Eingrenzen dieser Ressourcen mit using ist die strukturelle Lösung für diese Fehlerklasse.
Auf dem Server nehmen Node.js-FileHandle-Objekte aus fs/promises und viele Verbindungstypen bereits teil, und benutzerdefinierte Wrapper wie das obige DatabaseConnection-Beispiel decken den Rest ab.
Fehler während der Bereinigung mit SuppressedError behandeln
Bereinigung kann fehlschlagen, und sie kann fehlschlagen, während Ihr Code bereits einen Fehler wirft – ohne ein definiertes Verhalten würde ein Fehler den anderen stillschweigend überschreiben. JavaScript löst dies mit SuppressedError. Alle während der Bereinigung geworfenen Fehler, einschließlich des ursprünglichen Fehlers, der den Geltungsbereichsaustritt verursacht hat, werden in einem einzigen SuppressedError zusammengefasst – jede frühere Ausnahme als suppressed-Eigenschaft und die spätere Ausnahme als error-Eigenschaft – und er wird geworfen, nachdem die Bereinigung abgeschlossen ist.
Wenn also Ihr Block einen Fehler wirft und dann auch ein Disposer einen Fehler wirft, fangen Sie einen einzelnen SuppressedError ab, dessen .error der Bereinigungsfehler und dessen .suppressed der ursprüngliche Fehler ist. Wenn mehrere Disposer Fehler werfen, werden diese verschachtelt, sodass jeder Fehler erreichbar ist:
try {
using a = makeDisposableThatThrowsOnDispose("a");
using b = makeDisposableThatThrowsOnDispose("b");
throw new Error("body failed");
} catch (e) {
// e ist ein SuppressedError; b wird zuerst bereinigt und a zuletzt, also
// ist e.error der Bereinigungsfehler von a, und e.suppressed verschachtelt den Rest
// (Bereinigungsfehler von b, dann das ursprüngliche "body failed")
}
SuppressedError ist der Teil, der using sicher für fehlerintensiven Code macht.
Aktuelle Unterstützung: ausgeliefert, nicht „demnächst verfügbar”
Explicit Resource Management ist ein abgeschlossener TC39-Vorschlag, der als Teil von ES2026 standardisiert wurde – kein Stage-3-Experiment, das überall Transpilierung erfordert. Stand Mitte 2026 sieht die Lage wie folgt aus:
| Laufzeitumgebung | Status | Hinweise |
|---|---|---|
| Chrome / Edge / Opera | Ausgeliefert | using/await using seit Chromium 134 (V8 13.8); das Symbol Symbol.dispose allein ist seit Chrome 125 vorhanden |
| Node.js | Ausgeliefert | Natives using/await using in Node.js 24 (V8 13.6); Symbole seit 18.18.0 |
| Firefox | Ausgeliefert | Seit Firefox 141 (aktuell stabile Version 152) |
| Safari | Teilweise | Das Symbol.dispose-Symbol ist in Desktop-Safari 26.4 angekommen; Safari für iOS unterstützt es noch nicht |
| TypeScript | Ausgeliefert | using-Syntax seit 5.2; aktuell stabile Version 6.0 |
Eine Falle ist es wert, hervorgehoben zu werden: Ein definiertes Symbol.dispose-Symbol bedeutet nicht, dass die Engine using parsen kann. Das Symbol landet routinemäßig vor der Deklaration – Node hat die bekannten Symbole bereits in der v18-Linie (18.18.0) bereitgestellt, aber die Deklarationssyntax erst in Node 24 nativ gemacht, und Chrome hat das Symbol um v125 ausgeliefert, aber using erst ab 134 geparst. Auf einer älteren Engine können Sie also Symbol.dispose referenzieren und trotzdem einen Syntaxfehler oder einen „not disposable”-TypeError erhalten – und eine Unterstützungstabelle für Symbol.dispose (wie die oben verlinkte) läuft der tatsächlichen using-Unterstützung voraus. Vollständige using-Unterstützung ist eine Frage der Engine-Version, keine Polyfill-Frage.
Für Safari und ältere Ziele: transpilieren Sie. TypeScript erfordert das Setzen des Kompilierungsziels auf ES2022 oder niedriger und die Konfiguration von lib mit "esnext" oder "esnext.disposable" (TypeScript unterstützt die Syntax ab Version 5.2), und Sie können die Globals mit core-js oder dem disposablestack-Paket polyfüllen.
Der mechanische, fehleranfällige Bereinigungscode, den Sie bisher von Hand geschrieben haben, hat jetzt ein Sprachprimitiv: Fügen Sie [Symbol.dispose] oder [Symbol.asyncDispose] zu allem hinzu, das ein Teardown benötigt, deklarieren Sie es mit using oder await using, und die Laufzeitumgebung garantiert die Bereinigung in der richtigen Reihenfolge auf jedem Ausstiegspfad. Beginnen Sie damit, die eine Ressource einzuwickeln, die Sie am häufigsten vergessen zu schließen – eine Verbindung, ein Lock, ein Reader – und lassen Sie den Geltungsbereich den Rest erledigen.
Häufig gestellte Fragen
Was ist der Unterschied zwischen using und await using?
Eine using-Deklaration ruft die synchrone Symbol.dispose-Methode des Objekts auf, wenn der Block verlassen wird, während await using Symbol.asyncDispose aufruft und darauf wartet, sodass promise-basiertes Teardown abgeschlossen wird, bevor der Geltungsbereich geschlossen wird. Verwenden Sie using, wenn die Bereinigung synchron ist, und await using, wenn das Teardown ein Promise zurückgibt. Da await using auch einfache synchrone Disposables akzeptiert, können Sie es immer dann verwenden, wenn Sie nicht sicher sind, welche Art von Disposer ein Objekt besitzt.
Warum wirft using auf Node 18 einen 'not disposable'-Fehler, obwohl Symbol.dispose existiert?
Ein definiertes Symbol.dispose-Symbol bedeutet nicht, dass die Engine die using-Deklaration parsen kann. Node hat die bekannten Symbole Symbol.dispose und Symbol.asyncDispose ab Version 18.18.0 bereitgestellt, aber die vollständige using- und await using-Deklarationssyntax wurde erst in Node 24 nativ, das V8 13.6 ausliefert. Auf älteren Node-Versionen können Sie das Symbol referenzieren und trotzdem einen Syntaxfehler oder einen TypeError bei eingebauten Handles erhalten. Vollständige using-Unterstützung ist eine Frage der Engine-Version, keine Polyfill-Frage.
Ersetzt Explicit Resource Management die Garbage Collection?
Nein. Die Garbage Collection gibt Speicher nach einem eigenen, nicht-deterministischen Zeitplan frei und hat niemals Dateien geschlossen, Locks freigegeben oder Event-Listener abgemeldet. Explicit Resource Management behandelt diese Nicht-Speicher-Ressourcen deterministisch und führt ihre Bereinigung in dem Moment aus, in dem ein Geltungsbereich verlassen wird. Die beiden Mechanismen lösen unterschiedliche Probleme. WeakRef und FinalizationRegistry sind die GC-nahen Werkzeuge für den Speicher, während using und await using geltungsbereichsbasiertes Teardown von Datei-Handles, Sockets, Locks und Verbindungen bieten.
Wie verwende ich das using-Schlüsselwort mit einer Bibliothek, die keine dispose-Methode hat?
Hängen Sie das Symbol selbst mit Object.assign an. Ein Client, der eine close- oder end-Methode bereitstellt, aber kein Dispose-Symbol besitzt, wird in einer Zeile zu einem Disposable, indem eine asynchrone Symbol.asyncDispose-Methode zugewiesen wird, die die vorhandene close-Methode aufruft. Danach können Sie das Objekt mit await using deklarieren oder es mit einem AsyncDisposableStack use-Aufruf registrieren. Wenn Sie später auf eine Bibliotheksversion upgraden, die das Protokoll nativ implementiert, löst die Neuzuweisung einen Fehler aus, der Sie daran erinnert, den Shim zu entfernen.
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