Deterministisches Aufräumen von Ressourcen ohne try/finally-Boilerplate
Mit TypeScript 5.2 hielten using und await using Einzug, basierend auf dem TC39-Vorschlag für Explicit Resource Management. Ressourcen wie Dateihandles, Datenbankverbindungen oder Sperren werden damit automatisch und deterministisch freigegeben, sobald der Gültigkeitsbereich verlassen wird.
Inhaltsverzeichnis
- 1. Das Problem manueller Aufräumarbeiten
- 2. Grundlagen: das Symbol.dispose Protokoll
- 3. await using für asynchrone Ressourcen
- 4. Freigabereihenfolge bei mehreren Ressourcen
- 5. Kombination mit try/catch
- 6. DisposableStack für dynamische Ressourcenlisten
- 7. Einordnung im Vergleich zu anderen Sprachen
- 8. Typische Einsatzszenarien
- 9. Fallstricke und Laufzeit-Voraussetzungen
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem manueller Aufräumarbeiten
Ressourcen, die explizit freigegeben werden müssen, etwa offene Dateien, Netzwerkverbindungen, Datenbank-Transaktionen oder Sperren, erfordern traditionell ein try/finally-Konstrukt, damit die Freigabe auch bei einer geworfenen Exception zuverlässig erfolgt.
Bei mehreren Ressourcen innerhalb einer Funktion wird dieses Muster schnell unübersichtlich, weil jede zusätzliche Ressource eine weitere Verschachtelungsebene aus try/finally-Blöcken erfordert, um die korrekte Freigabereihenfolge sicherzustellen.
using Deklarationen lösen dieses Problem, indem der Compiler automatisch Code generiert, der die Freigabemethode der Ressource aufruft, sobald der umgebende Block verlassen wird, egal ob normal oder durch eine Exception.
2. Grundlagen: das Symbol.dispose Protokoll
Eine Ressource muss dem Disposable-Protokoll folgen, das heißt eine Methode unter dem wohlbekannten Symbol Symbol.dispose bereitstellen. Wird eine solche Ressource mit using statt const deklariert, ruft der Compiler diese Methode automatisch am Ende des Blocks auf.
Das folgende Beispiel zeigt eine einfache Ressourcenklasse und ihre Verwendung mit using. Die Konsolenausgabe demonstriert, dass die Freigabe exakt beim Verlassen des Blocks erfolgt, nicht erst am Ende der Funktion oder gar nie.
Wichtig ist, dass using nicht auf eine bestimmte Klasse von Ressourcen beschränkt ist: Jedes beliebige Objekt, das die Methode unter Symbol.dispose bereitstellt, kann mit using deklariert werden, unabhängig davon, ob es sich um eine eigene Klasse oder eine Instanz einer Drittanbieter-Bibliothek handelt.
class FileHandle implements Disposable {
constructor(private path: string) {
console.log(`open: ${this.path}`);
}
[Symbol.dispose]() {
console.log(`close: ${this.path}`);
}
}
function readConfig() {
using handle = new FileHandle("config.json");
// handle.dispose() wird automatisch aufgerufen, sobald der Block endet
}
readConfig();
// Ausgabe: open: config.json
// Ausgabe: close: config.json
3. await using für asynchrone Ressourcen
Für Ressourcen, deren Freigabe selbst asynchron ist, etwa eine Datenbankverbindung, die beim Schließen noch ausstehende Anfragen abwarten muss, gibt es die Variante await using. Sie erwartet eine Methode unter Symbol.asyncDispose, die ein Promise zurückgibt.
Der Compiler fügt an der entsprechenden Stelle automatisch ein await ein, sodass die Ressource vollständig geschlossen ist, bevor der umgebende async-Kontext fortfährt.
Eine Klasse kann sowohl Symbol.dispose als auch Symbol.asyncDispose gleichzeitig implementieren, um sowohl mit using als auch mit await using verwendet werden zu können, wobei die synchrone Variante meist eine vereinfachte, sofortige Freigabe ohne Wartezeit darstellt.
class DbConnection implements AsyncDisposable {
async [Symbol.asyncDispose]() {
await this.flushPendingQueries();
console.log("connection closed");
}
private async flushPendingQueries() {
// wartet auf offene Anfragen
}
}
async function query() {
await using db = new DbConnection();
// Verbindung wird nach Verlassen des Blocks asynchron geschlossen
}
4. Freigabereihenfolge bei mehreren Ressourcen
Werden mehrere Ressourcen im selben Block mit using deklariert, gibt der Compiler sie in umgekehrter Deklarationsreihenfolge frei, also nach dem LIFO-Prinzip, ähnlich einem Stapel. Das entspricht dem intuitiven Verhalten von verschachtelten try/finally-Blöcken, spart dabei aber die manuelle Verschachtelung komplett.
Diese Reihenfolge ist besonders wichtig, wenn Ressourcen voneinander abhängen, etwa eine Transaktion, die vor der zugrunde liegenden Verbindung geschlossen werden muss.
Ohne using müsste diese Reihenfolge manuell durch entsprechend verschachtelte try/finally-Blöcke nachgebildet werden, was mit jeder zusätzlichen Ressource an Lesbarkeit verliert. Mit using bleibt der Code dagegen flach und übersichtlich, unabhängig von der Anzahl der beteiligten Ressourcen.
function work() {
using a = openResource("A");
using b = openResource("B");
using c = openResource("C");
// Freigabe erfolgt in der Reihenfolge C, B, A
}
5. Kombination mit try/catch
using Deklarationen ersetzen try/finally für die Freigabe, ersetzen aber nicht try/catch für die Fehlerbehandlung. Beide lassen sich problemlos kombinieren: Die Freigabe erfolgt garantiert, unabhängig davon, ob ein catch-Block den Fehler auffängt oder die Exception weiter nach oben propagiert.
Wirft sowohl der Hauptcode als auch die Freigabemethode selbst einen Fehler, fasst die Laufzeit beide zu einem AggregateError zusammen, sodass keine Information über verlorene Fehler verloren geht.
function process() {
try {
using res = acquireResource();
riskyOperation();
} catch (err) {
console.error("Fehler behandelt:", err);
}
}
6. DisposableStack für dynamische Ressourcenlisten
Manchmal steht die Anzahl der Ressourcen erst zur Laufzeit fest, etwa bei einer Schleife, die dynamisch mehrere Verbindungen öffnet. Für diesen Fall stellt die Standardbibliothek DisposableStack und AsyncDisposableStack bereit, denen Ressourcen dynamisch hinzugefügt werden können.
Der Stack selbst implementiert ebenfalls Symbol.dispose, sodass er zusammen mit einer using Deklaration verwendet werden kann und beim Verlassen des Blocks alle enthaltenen Ressourcen in der richtigen Reihenfolge freigibt.
function openAll(paths: string[]) {
using stack = new DisposableStack();
const handles = paths.map((p) => stack.use(new FileHandle(p)));
return handles.length;
// alle Handles werden hier automatisch geschlossen
}
7. Einordnung im Vergleich zu anderen Sprachen
Das Muster hinter using ist in anderen Sprachen längst etabliert. C# kennt seit Jahren eine using-Anweisung mit fast identischer Semantik, Python löst dasselbe Problem über den with-Kontextmanager und das __exit__-Protokoll. TypeScript übernimmt mit Symbol.dispose bewusst ein vergleichbares, aber JavaScript-idiomatisches Konzept, das auf wohlbekannten Symbolen statt auf Sprachkeywords wie in C# aufbaut.
Der entscheidende Unterschied zu diesen Vorbildern liegt darin, dass Symbol.dispose Teil des TC39-Vorschlags ist und damit langfristig direkt in der JavaScript-Laufzeit selbst landet, nicht nur als TypeScript-spezifisches Compiler-Feature. Node.js und moderne Browser-Engines unterstützen das Protokoll bereits nativ, sodass der von TypeScript erzeugte Code in aktuellen Umgebungen ohne zusätzliche Bibliothek lauffähig ist.
Für Teams, die aus objektorientierten Sprachen zu TypeScript wechseln, erleichtert diese Ähnlichkeit den Einstieg erheblich, da sich das mentale Modell von Ressourcen-Lebenszyklen direkt übertragen lässt, ohne ein völlig neues Paradigma erlernen zu müssen.
8. Typische Einsatzszenarien
using eignet sich für alle Ressourcen mit einem klaren Lebenszyklus: Dateihandles, Netzwerksockets, Datenbankverbindungen und Transaktionen, Sperren in nebenläufigen Systemen, sowie Performance-Messungen, bei denen ein Timer beim Verlassen eines Blocks automatisch gestoppt und protokolliert werden soll.
Auch in Testcode ist das Muster wertvoll, etwa um temporäre Testumgebungen oder Mocks zuverlässig zurückzusetzen, ohne in jedem Test einen eigenen afterEach-Hook zu pflegen.
In Frontend-Anwendungen bietet sich using zudem für das Aufräumen von Event-Listenern, Observern oder Abonnements an, die sonst leicht vergessen werden und zu Speicherlecks führen, wenn eine Komponente entfernt wird, während die Registrierung weiterhin aktiv bleibt.
9. Fallstricke und Laufzeit-Voraussetzungen
using und await using sind sowohl ein Sprachfeature als auch ein Laufzeit-Feature: Der Compiler generiert Aufrufe von Symbol.dispose beziehungsweise Symbol.asyncDispose, die zur Laufzeit tatsächlich existieren müssen. In Umgebungen ohne native Unterstützung für diese wohlbekannten Symbole ist ein Polyfill erforderlich.
Zudem muss das Kompilierziel mindestens ES2022 sein, oder es muss über die Compiler-Option lib die entsprechende Symbol-Definition eingebunden werden. Wird using in einem Projekt mit älterem Zielstandard verwendet, meldet der Compiler einen Fehler wegen der fehlenden Typdefinitionen für Symbol.dispose.
Ein weiterer, oft übersehener Punkt: using Deklarationen sind block-gebunden wie let und const, nicht funktionsgebunden. Wird eine Ressource innerhalb einer Schleife mit using deklariert, erfolgt die Freigabe bei jedem Schleifendurchlauf erneut, was bei unbedachter Verwendung zu deutlich häufigerem Öffnen und Schließen der Ressource führen kann, als eigentlich beabsichtigt und sinnvoll war.
Schließlich sollte man beachten, dass eine using Variable nicht neu zugewiesen werden kann, ähnlich wie bei const. Wer eine Ressource innerhalb desselben Blocks austauschen möchte, muss dafür einen neuen Block oder eine eigene, klar benannte Hilfsfunktion verwenden.
In bestehenden Codebasen empfiehlt sich eine schrittweise Einführung, beginnend mit klar abgegrenzten Modulen, um Erfahrung mit dem neuen Freigabeverhalten zu sammeln, bevor using projektweit zum Standard für Ressourcenverwaltung wird und alte try/finally-Blöcke nach und nach vollständig ablöst und ersetzt.
| Merkmal | try/finally | using | await using |
|---|---|---|---|
| Automatische Freigabe | Manuell im finally | Automatisch | Automatisch, asynchron |
| Protokoll | Kein festes Protokoll | Symbol.dispose | Symbol.asyncDispose |
| Mehrere Ressourcen | Verschachtelung nötig | LIFO automatisch | LIFO automatisch |
| Fehlerbündelung | Manuell | AggregateError automatisch | AggregateError automatisch |
| Mindestversion | Alle Versionen | TypeScript 5.2+ | TypeScript 5.2+ |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
using Deklarationen
Seit Version
TypeScript 5.2
Basiert auf
TC39 Explicit Resource Management
Freigabereihenfolge
LIFO, wie ein Stapel
Typischer Einsatz
Dateien, Verbindungen, Sperren