JS
() =>
JavaScript · Ressourcen-Management · TC39 · Node.js
JavaScript using und await using
Explizites Ressourcen-Management ohne try/finally

Das neue using-Schlüsselwort in JavaScript automatisiert das Freigeben von Ressourcen beim Verlassen eines Scopes — Datenbankverbindungen, Datei-Handles, Locks und Worker werden garantiert aufgeräumt, egal ob der Block normal endet, eine Exception wirft oder ein early return ausgelöst wird.

11 Min. Lesezeit TC39 Stage 4 · TypeScript 5.2+ · Node.js 22+ using · await using · Symbol.dispose · DisposableStack

1. Das Problem mit try/finally und manuellem Cleanup

Ressourcen-Leaks sind eine der häufigsten und schwer zu findenden Fehlerklassen in JavaScript-Anwendungen. Datenbankverbindungen, die nicht zurückgegeben werden, Datei-Handles, die geöffnet bleiben, Locks, die nicht freigegeben werden, und Worker-Threads, die nicht terminiert werden — all das führt zu graduell schlechter werdender Performance, erschöpften Verbindungspools und schwer reproduzierbaren Bugs. Das Problem entsteht fast immer durch frühe Returns, unerwartete Exceptions oder schlicht vergessenes Cleanup-Code.

Das klassische Muster zur Vermeidung ist try/finally: Die Ressource wird vor dem try-Block erstellt, im finally-Block freigegeben — egal was im try-Block passiert. Das Muster funktioniert, hat aber erhebliche ergonomische Probleme: Es zwingt zu verschachteltem Code, trennt Erstellung und Cleanup weit voneinander, macht den Code schwer lesbar und skaliert schlecht mit mehreren Ressourcen, die in umgekehrter Reihenfolge freigegeben werden müssen. Das neue using-Schlüsselwort in JavaScript löst genau dieses Problem — als sprachliches Konstrukt, das Ressourcen-Cleanup garantiert, direkt dort deklariert, wo die Ressource erstellt wird.


// BEFORE: try/finally — verbose, error-prone, hard to read
async function processData() {
  const connection = await db.connect();
  let result;
  try {
    const fileHandle = await fs.open("data.csv", "r");
    try {
      const lock = await acquireLock("data-processing");
      try {
        result = await compute(connection, fileHandle, lock);
      } finally {
        await lock.release(); // must happen even on exception
      }
    } finally {
      await fileHandle.close(); // must happen even if lock.release() throws
    }
  } finally {
    await connection.close(); // must happen last
  }
  return result;
}

// AFTER: using — reads linearly, cleanup guaranteed at scope end
async function processData() {
  await using connection = await db.connect();  // auto-closes at scope end
  await using fileHandle = await fs.open("data.csv", "r"); // auto-closes
  await using lock = await acquireLock("data-processing"); // auto-releases

  return await compute(connection, fileHandle, lock);
  // All three dispose in reverse order when scope ends — always
}

2. using: automatisches Cleanup beim Scope-Ende

Das using-Schlüsselwort funktioniert wie const und let — es deklariert eine Variable und bindet sie an einen Scope. Der entscheidende Unterschied: Beim Verlassen des Scopes ruft die JavaScript-Engine automatisch die [Symbol.dispose]()-Methode des Objekts auf. Das passiert in allen Fällen: normales Scope-Ende, return-Statement, break, continue und auch wenn eine Exception geworfen wird. Mehrere using-Deklarationen im selben Scope werden in umgekehrter Reihenfolge ihrer Deklaration disposed — LIFO (Last In, First Out), wie es für korrekte Ressourcen-Freigabe notwendig ist.

Ein mit using deklariertes Objekt ist wie const unveränderbar — man kann die Variable nicht neu zuweisen. Das Objekt selbst kann verändert werden, aber die Bindung ist fix, was das Muster einfacher zu verstehen macht: Die Ressource ist an den Scope gebunden, der Scope endet, die Ressource wird freigegeben. Das ist die semantische Analogie zu C#'s using-Statement, Python's Context Managern (with) und Java's try-with-resources. JavaScript hatte lange kein entsprechendes Konstrukt — using schließt diese Lücke als echtes Sprachfeature, nicht als Bibliothek.

3. Symbol.dispose implementieren: eigene Ressourcen disposable machen

Jedes Objekt, das mit using verwendet werden soll, muss die Methode [Symbol.dispose]() implementieren. Das ist ein Well-Known Symbol — ein spezielles, globales Symbol, das die JavaScript-Engine kennt und beim Scope-Ende aufruft. Die Implementierung ist einfach: Die Methode enthält den Cleanup-Code, der normalerweise im finally-Block stehen würde. Wichtig: [Symbol.dispose]() kann synchron oder — für await using — asynchron via [Symbol.asyncDispose]() sein.

Für Klassen, die eigene Ressourcen verwalten — Verbindungen, Handles, Locks — ist die Implementierung von [Symbol.dispose]() die moderne API-Ergonomie. Nutzer der Klasse können dann using statt manueller close()-Aufrufe verwenden. Das reduziert nicht nur Fehler durch vergessenes Cleanup, sondern macht den Code deklarativer: Die Ressource ist an den Scope gebunden — sobald der Scope endet, ist die Ressource freigegeben. Das ist die korrekte mentale Abstraktion für Ressourcen-Management.


// Implementing Symbol.dispose on a custom resource class
class DatabaseConnection {
  #pool;
  #client;

  constructor(pool, client) {
    this.#pool = pool;
    this.#client = client;
  }

  async query(sql, params) {
    return this.#client.query(sql, params);
  }

  // Synchronous dispose: release connection back to pool
  [Symbol.dispose]() {
    this.#pool.release(this.#client);
    console.log("Connection returned to pool");
  }
}

// Factory function — returns a Disposable resource
function acquireConnection(pool) {
  const client = pool.acquire();
  return new DatabaseConnection(pool, client);
}

// Usage: using handles cleanup automatically
function runQuery(pool, userId) {
  using conn = acquireConnection(pool);
  // conn[Symbol.dispose]() is called when this scope ends
  return conn.query("SELECT * FROM users WHERE id = $1", [userId]);
  // No need for try/finally — connection is always returned to pool
}

// Works with early returns too
function findUser(pool, userId) {
  using conn = acquireConnection(pool);
  const result = conn.query("SELECT * FROM users WHERE id = $1", [userId]);
  if (!result.rows.length) return null; // dispose() called here too
  return result.rows[0];
} // dispose() called here on normal exit

4. await using: asynchrones Cleanup mit Symbol.asyncDispose

await using ist die asynchrone Variante von using. Wenn eine Ressource beim Cleanup asynchrone Arbeit ausführen muss — eine Netzwerkverbindung schließen, eine Transaktion abschließen, einen Remote-Lock freigeben — dann implementiert das Objekt [Symbol.asyncDispose](), das eine Promise zurückgibt. Mit await using wartet die JavaScript-Engine auf die Erfüllung dieser Promise, bevor die nächste Ressource disposed wird und bevor der aufrufende Code fortgesetzt wird.

await using darf nur in async-Funktionen verwendet werden — analog zu await. Das ist konsequent: Der Aufrufer muss ebenfalls async sein, um auf das Cleanup zu warten. Wenn man einen asynchronen Cleanup braucht, aber den Aufrufer nicht async machen kann, ist DisposableStack mit manuell registrierten Cleanup-Funktionen die Alternative. Für die meisten Server-seitigen Anwendungen mit Node.js ist await using jedoch die natürliche Wahl: Datenbankverbindungen, HTTP-Clients und File-System-Handles haben fast immer asynchrone Close-Methoden.

5. DisposableStack: mehrere Ressourcen zusammen verwalten

DisposableStack und AsyncDisposableStack sind die ergonomischen Wrapper für komplexere Ressourcen-Management-Szenarien. Ein DisposableStack sammelt mehrere Cleanup-Aktionen und führt sie beim Dispose in umgekehrter Reihenfolge aus — wie ein Stack, bei dem als letztes eingefügte Aktionen als erstes ausgeführt werden. Das ist besonders nützlich, wenn man Ressourcen nicht mit using deklarieren kann — etwa weil sie in einer Schleife erstellt werden oder weil man bedingte Cleanup-Logik braucht.

Die defer()-Methode des DisposableStack akzeptiert eine beliebige Funktion als Cleanup-Aktion — das erlaubt das Registrieren von Callbacks für jede Art von Cleanup, nicht nur für Objekte mit [Symbol.dispose](). Mit move() kann ein DisposableStack seinen Inhalt an einen anderen Stack übertragen — das ist nützlich, wenn eine Factory-Funktion mehrere Ressourcen erstellt und sie im Erfolgsfall alle zusammen an den Aufrufer übergeben möchte, im Fehlerfall aber alle gleichzeitig aufräumt.


// AsyncDisposableStack: manage multiple async resources together
async function processWithMultipleResources(config) {
  await using stack = new AsyncDisposableStack();

  // Register any async cleanup function
  const connection = await db.connect(config.dbUrl);
  stack.defer(async () => {
    await connection.close();
    console.log("Database connection closed");
  });

  const cache = await redisClient.connect(config.redisUrl);
  stack.defer(async () => {
    await cache.quit();
    console.log("Redis connection closed");
  });

  // Register a Disposable object directly
  const lock = await acquireLock("process-lock");
  stack.use(lock); // calls lock[Symbol.asyncDispose]() when stack disposes

  // All resources are released in reverse order (LIFO) when scope ends:
  // 1. lock (Symbol.asyncDispose)
  // 2. cache (defer callback)
  // 3. connection (defer callback)
  return await doWork(connection, cache);
}

// move(): transfer ownership to caller on success
async function buildResources(config) {
  await using tempStack = new AsyncDisposableStack();

  // Acquire resources — if anything fails, tempStack cleans up
  const conn = await db.connect(config.dbUrl);
  tempStack.defer(async () => conn.close());

  const cache = await redis.connect(config.cacheUrl);
  tempStack.defer(async () => cache.quit());

  // Transfer ownership to caller — tempStack is now empty
  return { conn, cache, dispose: tempStack.move() };
}

6. Bestehende APIs adaptieren: Wrapper ohne Codeänderung

Bestehende JavaScript-APIs haben keine [Symbol.dispose]()-Methode — sie müssen adaptiert werden. Für APIs, deren Quellcode man kontrolliert, ist die Implementierung von [Symbol.dispose]() direkt auf der Klasse der sauberste Weg. Für externe Bibliotheken ohne [Symbol.dispose]() gibt es Wrapper-Funktionen: Sie nehmen das Ressourcen-Objekt, hängen eine [Symbol.dispose]()-Methode an und geben das erweiterte Objekt zurück.

Ein elegantes Muster: Eine generische asDisposable(resource, disposeFn)-Hilfsfunktion, die ein beliebiges Objekt mit einer [Symbol.dispose]()-Methode versieht. Das erlaubt das Verwenden von using mit jeder API, ohne deren Quellcode zu ändern. Für Node.js-Streams, EventEmitter und andere async Ressourcen gibt es entsprechende asAsyncDisposable()-Wrapper. Die Community hat begonnen, solche Wrapper als kleine Bibliotheken bereitzustellen — aber für die meisten Fälle ist ein lokaler Wrapper in fünf Zeilen ausreichend.

7. using vs. try/finally vs. Context Manager im Vergleich

Der Vergleich mit anderen Sprachen zeigt, dass JavaScript mit using ein langes Desiderat umsetzt. Python hat Context Manager mit with seit Version 2.5, C# hat using seit Version 1.0, Java try-with-resources seit Java 7. JavaScript war die einzige der großen Sprachen ohne ein Ressourcen-Management-Konstrukt — using schließt diese Lücke.

Aspekt try/finally using (JS) with (Python)
Cleanup-Garantie Ja, bei korrekter Implementierung Ja, sprachseitig garantiert Ja, sprachseitig garantiert
Lesbarkeit Verschachtelt bei mehreren Ressourcen Linear, deklarativ Linear, deklarativ
LIFO-Reihenfolge Manuell sicherstellen Automatisch Automatisch
Async Cleanup Ja, mit await im finally Ja, mit await using Erst ab Python 3.10 (asynccontextmanager)
Fehlerrisiko Hoch — kann vergessen werden Minimal — strukturell garantiert Minimal — strukturell garantiert

Der entscheidende Vorteil von using gegenüber try/finally ist nicht Kürze, sondern strukturelle Korrektheit. Mit try/finally ist es möglich, das finally-Block zu vergessen, die Ressource falsch zu referenzieren oder die Reihenfolge bei mehreren Ressourcen falsch zu machen. Mit using sind diese Fehlerklassen strukturell unmöglich — der Compiler (TypeScript) oder die Laufzeitumgebung erzwingt das korrekte Verhalten.

8. TypeScript-Integration und Disposable-Interface

TypeScript unterstützt using ab Version 5.2 vollständig. Die Typdefinitionen beinhalten die Interfaces Disposable und AsyncDisposable: Disposable erfordert die Methode [Symbol.dispose](): void, AsyncDisposable erfordert [Symbol.asyncDispose](): Promise<void>. Klassen, die diese Interfaces implementieren, können mit using bzw. await using deklariert werden — der TypeScript-Compiler prüft die Kompatibilität statisch und gibt einen Fehler, wenn ein Objekt ohne [Symbol.dispose]() mit using deklariert wird.

Das TypeScript-Typsystem macht using besonders wertvoll: Bibliotheks-Autoren können ihre Ressourcen-Klassen als Disposable oder AsyncDisposable typisieren und dadurch signalisieren, dass sie mit using verwendet werden sollen. IDEs wie PhpStorm und VS Code zeigen den Disposable-Status direkt im Autocomplete an — das ist eine ergonomische Verbesserung gegenüber dem impliziten Wissen, dass man close() aufrufen muss.


// TypeScript 5.2+ — Disposable and AsyncDisposable interfaces
interface DatabasePool {
  acquire(): Promise<PooledConnection>;
  release(conn: PooledConnection): void;
}

class PooledConnection implements AsyncDisposable {
  readonly #pool: DatabasePool;
  readonly #conn: RawConnection;

  constructor(pool: DatabasePool, conn: RawConnection) {
    this.#pool = pool;
    this.#conn = conn;
  }

  async query<T>(sql: string, params: unknown[]): Promise<T[]> {
    return this.#conn.query<T>(sql, params);
  }

  // TypeScript checks that this matches AsyncDisposable interface
  async [Symbol.asyncDispose](): Promise<void> {
    await this.#conn.rollbackIfActive();
    this.#pool.release(this.#conn);
  }
}

// using enforced: TypeScript error if PooledConnection lacks [Symbol.asyncDispose]
async function getUser(pool: DatabasePool, id: string) {
  await using conn = new PooledConnection(pool, await pool.acquire());
  const [user] = await conn.query<User>("SELECT * FROM users WHERE id=$1", [id]);
  return user ?? null;
  // conn[Symbol.asyncDispose]() guaranteed to run — rollback + pool release
}

// Helper: adapt existing APIs without modifying their source
function asDisposable<T>(resource: T, dispose: (r: T) => void): T & Disposable {
  return Object.assign(resource as T & Disposable, {
    [Symbol.dispose]() { dispose(resource); }
  });
}

// Usage: use any API with using
function readFile(path: string) {
  using fd = asDisposable(openFileSync(path), (f) => f.closeSync());
  return fd.readAllText();
}

9. Fehlerbehandlung: was passiert wenn dispose() wirft?

Was passiert, wenn [Symbol.dispose]() eine Exception wirft — besonders in Kombination mit einer Exception aus dem Hauptcode? Das ist ein Szenario, das bei try/finally bekannte Probleme hat: Wirft der finally-Block eine Exception, überschreibt sie die ursprüngliche Exception aus dem try-Block — die Information über den ursprünglichen Fehler geht verloren. using löst das mit einem neuen Exception-Typ: SuppressedError. Wenn sowohl der Hauptcode als auch [Symbol.dispose]() eine Exception werfen, enthält SuppressedError beide — die ursprüngliche Exception in error und die Cleanup-Exception in suppressed. Kein Fehler geht verloren.

Das Fehlerverhalten bei mehreren using-Deklarationen im selben Scope ist ebenfalls durchdacht: Wenn das Dispose eines Objekts wirft, versucht die Engine trotzdem, alle weiteren Objekte in LIFO-Reihenfolge zu disposen. Alle Exceptions werden gesammelt und als verschachtelte SuppressedError-Kette zurückgegeben. Das bedeutet: Auch wenn mehrere Cleanup-Aktionen fehlschlagen, werden alle versucht — es gibt kein frühes Abbrechen des Dispose-Prozesses. Das ist das korrekte Verhalten für Ressourcen-Management, bei dem alle Ressourcen freigegeben werden müssen, auch wenn einzelne Freigaben scheitern.

10. Zusammenfassung

Das using-Schlüsselwort und await using sind das fehlende Stück für robustes Ressourcen-Management in JavaScript. Sie ersetzen das fehleranfällige try/finally-Muster durch ein strukturelles Sprachkonstrukt, das Cleanup beim Verlassen eines Scopes garantiert — egal ob durch normales Ende, return, break oder Exception. Die Implementierung ist minimal: Eine Methode [Symbol.dispose]() oder [Symbol.asyncDispose]() auf dem Ressourcen-Objekt reicht. DisposableStack und AsyncDisposableStack erweitern das Muster für komplexere Szenarien mit dynamisch registrierten Cleanup-Aktionen.

Mit TypeScript 5.2+ ist using vollständig typisiert und statisch geprüft — TypeScript verhindert die Verwendung von Objekten ohne [Symbol.dispose]() mit using. Für Node.js sind Polyfills verfügbar, nativer Support kommt mit V8-Updates. Das Muster, das Python-Entwickler mit with und C#-Entwickler mit using längst kennen, ist jetzt in JavaScript angekommen — sauber, erweiterbar und ohne Abhängigkeit von externen Bibliotheken.

Mironsoft

JavaScript-Modernisierung, Node.js-Architektur und Ressourcen-Management

Ressourcen-Leaks in Node.js beseitigen?

Wir analysieren bestehende Node.js-Anwendungen auf Ressourcen-Leaks, migrieren try/finally-Muster zu using und implementieren robustes Ressourcen-Management mit Symbol.dispose.

Leak-Analyse

Verbindungs-Leaks, nicht geschlossene Handles und fehlende Cleanup-Pfade in Node.js-Apps identifizieren

using-Migration

try/finally-Muster zu using migrieren, Symbol.dispose implementieren und DisposableStack einsetzen

TypeScript-Integration

Disposable-Interfaces, TypeScript 5.2+ und statisch geprüftes Ressourcen-Management einführen

JavaScript using — Das Wichtigste auf einen Blick

Voraussetzung: Symbol.dispose

Jedes Objekt, das mit using deklariert wird, muss [Symbol.dispose]() implementieren. Für async: [Symbol.asyncDispose]() mit Promise-Rückgabe. TypeScript 5.2+ prüft das statisch.

Scope-Ende bedeutet Dispose

Cleanup passiert bei normalem Ende, return, break, continue und Exceptions. LIFO-Reihenfolge bei mehreren using-Deklarationen. Kein manuelles try/finally nötig.

DisposableStack

Für dynamisch registrierte Cleanup-Aktionen. defer() akzeptiert beliebige Funktionen. use() akzeptiert Disposable-Objekte. move() überträgt Ownership an anderen Stack.

SuppressedError

Wenn Hauptcode und dispose() beide werfen, entstehen keine verlorenen Exceptions — SuppressedError enthält beide. LIFO-Dispose wird trotzdem vollständig durchgeführt.

11. FAQ: JavaScript using und Ressourcen-Management

1Was ist das using-Schlüsselwort?
Variablen-Deklaration, die beim Scope-Ende automatisch [Symbol.dispose]() aufruft. Ersetzt try/finally — Cleanup garantiert bei normalem Ende, return, break und Exceptions.
2Was braucht ein Objekt für using?
Die Methode [Symbol.dispose]() für sync oder [Symbol.asyncDispose]() für async (await using). TypeScript 5.2+ prüft das statisch über die Disposable/AsyncDisposable Interfaces.
3using vs. await using?
using: synchron, ruft [Symbol.dispose](). await using: asynchron, ruft [Symbol.asyncDispose]() und wartet auf Promise. Nur in async-Funktionen einsetzbar.
4Reihenfolge bei mehreren using?
LIFO — Last In, First Out. Die zuletzt deklarierte Ressource wird als erste disposed. Korrekte Reihenfolge für Ressourcen-Management ohne manuelles Tracking.
5Was ist DisposableStack?
Sammelt Cleanup-Aktionen via defer() (beliebige Funktionen) und use() (Disposable-Objekte). LIFO beim Dispose. move() überträgt Ownership — nützlich für Factory-Funktionen.
6Wenn dispose() eine Exception wirft?
SuppressedError: enthält ursprüngliche Exception in .error und Cleanup-Exception in .suppressed. Keine Exception geht verloren. LIFO-Dispose läuft trotzdem vollständig durch.
7Bestehende APIs ohne Symbol.dispose?
Adapter: Object.assign(resource, { [Symbol.dispose]() { resource.close(); } }). Oder DisposableStack.defer() mit beliebiger Cleanup-Funktion — kein Symbol.dispose nötig.
8TypeScript-Version für using?
Ab TypeScript 5.2. Disposable und AsyncDisposable Interfaces eingebaut. Statische Prüfung ob [Symbol.dispose]() vorhanden. Downlevel-Kompilierung zu try/finally für ältere Targets.
9Node.js-Unterstützung?
Nativ ab Node.js 22 (V8-Support). Für ältere Versionen: Babel oder TypeScript Downlevel-Kompilierung zu try/finally. TypeScript transpiliert using automatisch für ältere Targets.
10using vs. Garbage Collection?
GC gibt Speicher frei — nicht deterministisch, nicht für externe Ressourcen. using gibt Verbindungen, Handles und Locks deterministisch beim Scope-Ende frei — unabhängig vom GC-Zyklus.