Comment JavaScript a enfin appris à faire le ménage
Gestion explicite des ressources en JavaScript avec using, await using, DisposableStack et SuppressedError pour nettoyer fichiers, verrous, sockets et connexions.
La nouvelle fonctionnalité de gestion explicite des ressources de JavaScript — les déclarations using et await using reposant sur Symbol.dispose et Symbol.asyncDispose — offre au langage un nettoyage déterministe et délimité par portée des ressources non-mémoire telles que les descripteurs de fichiers, les sockets, les verrous et les connexions de base de données. Il ne s’agit pas de garbage collection. Le garbage collector récupère la mémoire selon son propre calendrier non déterministe, et il n’a jamais fermé vos fichiers, libéré vos verrous ni désabonné vos écouteurs d’événements. Ce sont précisément ces ressources que using nettoie, de manière prévisible, dès qu’une portée se termine.
Si vous avez eu l’habitude d’écrire manuellement des blocs try/finally pour fermer des connexions et libérer des verrous, cette fonctionnalité remplace ce code répétitif par un simple mot-clé. Cet article explique ce que fait using, comment fonctionne la distinction synchrone/asynchrone, comment écrire vos propres objets jetables (disposables), comment en coordonner plusieurs avec DisposableStack, comment adapter des bibliothèques qui ne le supportent pas encore, et comment les erreurs de nettoyage remontent via le nouveau SuppressedError. (WeakRef et FinalizationRegistry sont les outils liés au GC pour la mémoire ; il s’agit ici d’un mécanisme entièrement différent.)
Points clés à retenir
- Une déclaration
usingappelle la méthode[Symbol.dispose]()d’un objet lorsque le bloc englobant se termine ;await usingappelle[Symbol.asyncDispose]()et l’attend, de sorte que le nettoyage renvoyant une promesse se termine avant la fermeture de la portée. - Lorsque plusieurs ressources partagent une même portée, elles sont supprimées dans l’ordre inverse de leur déclaration, de sorte qu’une ressource dépendante est nettoyée avant la dépendance sur laquelle elle repose.
- La gestion explicite des ressources est une proposition TC39 finalisée et standardisée dans ES2026, disponible nativement dans Chrome 134 (V8 13.8), Firefox 141 et Node.js 24 (V8 13.6) ; Safari est encore en cours de rattrapage — le symbole
Symbol.disposeest disponible dans Safari desktop 26.4, mais pas encore dans Safari sur iOS. DisposableStack.prototype.move()transfère les ressources enregistrées dans une nouvelle pile et marque l’originale comme supprimée sans rien nettoyer — c’est le patron de conception pour transmettre en toute sécurité des ressources partiellement construites hors d’un constructeur.- Si le nettoyage lève une exception alors que votre code en lève déjà une, JavaScript encapsule les deux dans un
SuppressedError, de sorte qu’aucune des deux erreurs — ni l’échec original ni l’échec du nettoyage — n’est perdue.
Le problème de nettoyage que try/finally n’a jamais bien résolu
Chaque ressource que vous ouvrez — un descripteur de fichier, un lecteur de flux, une connexion de base de données, un verrou — doit être fermée, et le seul outil au niveau du langage pour le garantir était try/finally. Cela fonctionne, mais c’est verbeux et cela passe mal à l’échelle. Prenons l’exemple d’un lecteur Web Streams : si une erreur survient pendant la lecture et que vous oubliez d’appeler releaseLock() avant que l’erreur ne se propage, le flux reste verrouillé. La solution consiste à envelopper la boucle de lecture dans un bloc try et à placer reader.releaseLock() dans le finally afin que le verrou soit toujours libéré.
Ce patron de conception convient pour une seule ressource. Avec deux ou trois ressources interdépendantes, on se retrouve avec des blocs finally imbriqués où l’ordre est important et où une exception levée dans un nettoyage peut en court-circuiter d’autres. La partie mécanique — « cette chose nécessite un nettoyage, exécutez-le à la sortie, dans le bon ordre, même sur le chemin d’erreur » — est précisément ce que le langage gère désormais pour vous.
Comment fonctionne le mot-clé using
Discover how at OpenReplay.com.
Le mot-clé using déclare une liaison à portée de bloc, non réassignable (comme const), dont la valeur doit être null, undefined, ou un objet portant une méthode [Symbol.dispose]() ; lorsque la variable sort de portée, cette méthode s’exécute automatiquement. Utilisez using pour le nettoyage synchrone. Utilisez await using lorsque le nettoyage renvoie une promesse — il déclare des variables à portée de bloc qui sont supprimées de manière asynchrone, la valeur doit posséder une méthode [Symbol.asyncDispose]() ou [Symbol.dispose](), et cette méthode est appelée et attendue lorsque la variable sort de portée. Comme await using accepte également les disposables synchrones ordinaires, vous pouvez y recourir chaque fois que vous ne savez pas de quel type il s’agit.
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]() est appelé et attendu ici, sur chaque chemin de sortie
}
Les objets FileHandle de Node.js implémentent déjà le protocole de disposable asynchrone, donc cela fonctionne sans enveloppe manuelle. Notez les deux opérations await : await fs.open() attend l’acquisition et déroule la promesse en un FileHandle, tandis que await using attend le nettoyage lorsque la variable sort de portée.
La règle d’ordre est particulièrement importante lorsque les ressources dépendent les unes des autres. Si une portée contient plusieurs déclarations using ou await using, tous les nettoyeurs s’exécutent en séquence dans l’ordre inverse de déclaration, quel que soit le type de déclaration, et tous sont garantis de s’exécuter, à l’instar d’un bloc finally.
await using db = await openConnection(); // supprimé en second
await using tx = await db.beginTransaction(); // supprimé en premier
// tx est nettoyé avant db, de sorte que la connexion est encore active
// lorsque la transaction est finalisée
Concernant la portée : using est valide à l’intérieur de tout bloc, corps de fonction, bloc d’initialisation statique, en-tête for, et au niveau supérieur d’un module — mais pas au niveau supérieur d’un script, car les portées de script persistent et le nettoyeur ne se déclencherait jamais. await using au niveau supérieur est réservé aux modules, car il dépend de await au niveau supérieur. Les règles conformes à la spécification sont documentées dans la référence MDN sur using.
Écrire votre propre disposable
N’importe quel objet devient disposable en implémentant [Symbol.dispose]() pour le nettoyage synchrone ou [Symbol.asyncDispose]() pour le nettoyage asynchrone — il n’y a pas de classe de base ni d’étape d’enregistrement. Le symbole bien connu est le contrat : un objet est disposable s’il possède une méthode [Symbol.dispose](), et la déclaration using recherche ce symbole sur l’initialiseur pour trouver la méthode à appeler lorsque la variable sort de portée.
Voici un enveloppe de connexion de base de données utilisée tout au long de cet article :
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]() s'exécute ici — le client est libéré sur chaque chemin
}
Un nettoyeur synchrone suit la même structure avec [Symbol.dispose]() à la place. Un nettoyeur synchrone ne devrait pas renvoyer de promesse, car les promesses renvoyées par [Symbol.dispose]() ne sont pas attendues par await using ; pour déclarer des disposables asynchrones, utilisez Symbol.asyncDispose.
Coordonner des ressources avec DisposableStack
Lorsqu’un seul objet possède plusieurs ressources — ou lorsque vous acquérez des ressources dans une boucle — DisposableStack et AsyncDisposableStack les rassemblent et nettoient l’ensemble du groupe en une seule fois, dans l’ordre inverse. Les deux structures fournissent des méthodes telles que use(), adopt() et defer() pour ajouter des ressources ou des actions de nettoyage, ainsi qu’une méthode dispose() ou asyncDispose() pour déclencher le nettoyage. Elles portent elles-mêmes [Symbol.dispose]() / [Symbol.asyncDispose](), ce qui leur permet de fonctionner avec using et await using.
Les trois méthodes d’enregistrement couvrent différentes formes :
| Méthode | Enregistre | À utiliser quand |
|---|---|---|
use(resource) | Une ressource qui possède déjà un symbole de nettoyage | L’objet est lui-même disposable |
adopt(value, onDispose) | Une valeur non-disposable avec un rappel de nettoyage | La ressource a une méthode de style close/abort mais pas de symbole |
defer(onDispose) | Un rappel de nettoyage autonome, sans ressource | Vous devez exécuter un nettoyage arbitraire (ex. clearInterval) |
function startWorker() {
using stack = new DisposableStack();
const handle = setInterval(poll, 5000);
stack.defer(() => clearInterval(handle));
stack.adopt(openSocket(), (s) => s.close());
// les deux nettoyages s'exécutent, dans l'ordre inverse, à la sortie du bloc
}
La puissance non évidente réside dans move(), qui résout un vrai problème de sécurité à la construction. Parfois, la portée d’une fonction ne suffit pas — une classe ou un objet possède plusieurs ressources qui devraient être regroupées comme avec using, mais en tant que champ de classe ou fermeture, ce pour quoi DisposableStack et AsyncDisposableStack sont conçus. DisposableStack.prototype.move() transfère chaque ressource enregistrée dans une nouvelle pile et marque l’originale comme supprimée sans nettoyer les ressources, ce qui vous permet de construire des ressources localement et de les transmettre uniquement si la construction réussit entièrement :
function openResources() {
using cleanup = new DisposableStack();
const a = cleanup.use(openA());
const b = cleanup.use(openB()); // si cela lève une exception, `a` est supprimé automatiquement
const moved = cleanup.move(); // succès : la propriété est transférée, rien n'est supprimé
return moved; // l'appelant est maintenant responsable du nettoyage des deux
}
Si openB() lève une exception, le bloc se termine et cleanup supprime a — aucune fuite. Si tout réussit, move() transmet les ressources à l’appelant intactes. La sémantique de move() est documentée dans l’article de V8 sur la gestion explicite des ressources.
Adapter des bibliothèques qui ne supportent pas encore le nettoyage
Vous n’avez pas besoin qu’une bibliothèque adopte Symbol.dispose pour l’utiliser avec using — attachez le symbole vous-même avec Object.assign. Un client de bibliothèque qui expose une méthode close() ou end() mais pas de symbole de nettoyage devient disposable en une seule ligne. Assigner une méthode Symbol.asyncDispose sur le client vous permet de l’utiliser dans des déclarations await using et avec AsyncDisposableStack#use(), et si vous passez ultérieurement à une version qui implémente le protocole nativement, vous obtiendrez une erreur vous rappelant de supprimer le shim.
const client = await MongoClient.connect(url);
Object.assign(client, {
async [Symbol.asyncDispose]() {
await client.close();
},
});
async function run() {
await using db = client; // se nettoie maintenant correctement
// ...
}
Le shim Object.assign est le pont pour l’écosystème actuel : la plupart des clients tiers n’ont pas encore publié de symboles de nettoyage, et il vous permet de les intégrer dès aujourd’hui.
Où cela aide côté frontend et dans Node
Côté client, les ressources qui fuient sont les écouteurs d’événements, les instances IntersectionObserver/ResizeObserver, les navigator.locks, les Web Streams verrouillés et les transactions IndexedDB — toutes des choses qui ont une étape de libération explicite qu’il est facile d’omettre sur un chemin d’erreur. Les envelopper dans un disposable lie leur durée de vie à une portée. L’exemple canonique de l’équipe V8 est un lecteur de flux : une déclaration using sur un objet dont [Symbol.dispose]() appelle reader.releaseLock() supprime complètement le besoin de se souvenir de la libération.
Les observateurs orphelins et les verrous non libérés sont précisément les fuites qui échappent à un test local rapide. Un observateur orphelin ou un verrou non libéré ne coûte rien sur une page que vous rechargez toutes les quelques secondes, mais au fil d’une longue session d’application monopage, ils s’accumulent en une croissance mémoire progressive et des ralentissements d’interaction — la dégradation lente qui se révèle dans un enregistrement de session complet lorsqu’un simple rechargement ne reproduit jamais le problème. Délimiter ces ressources avec using est la correction structurelle pour cette catégorie de bogues.
Côté serveur, les objets FileHandle de Node.js issus de fs/promises et de nombreux types de connexions participent déjà, et des enveloppes personnalisées comme le DatabaseConnection ci-dessus couvrent le reste.
Gérer les erreurs lors du nettoyage avec SuppressedError
Le nettoyage peut échouer, et il peut échouer alors que votre code lève déjà une exception — sans comportement défini, une erreur en écraserait silencieusement une autre. JavaScript résout ce problème avec SuppressedError. Toutes les erreurs levées pendant le nettoyage, y compris l’erreur initiale qui a provoqué la sortie de portée, sont agrégées dans un SuppressedError — chaque exception antérieure comme propriété suppressed et l’exception ultérieure comme propriété error — et il est levé une fois le nettoyage terminé.
Ainsi, si votre bloc lève une exception et qu’un nettoyeur en lève également une, vous interceptez un unique SuppressedError dont .error est l’échec du nettoyage et dont .suppressed est l’original. Lorsque plusieurs nettoyeurs lèvent des exceptions, elles s’imbriquent, de sorte que chaque échec est accessible :
try {
using a = makeDisposableThatThrowsOnDispose("a");
using b = makeDisposableThatThrowsOnDispose("b");
throw new Error("body failed");
} catch (e) {
// e est un SuppressedError ; b est supprimé en premier et a en dernier, donc
// e.error est l'erreur de nettoyage de a, et e.suppressed imbrique le reste
// (l'erreur de nettoyage de b, puis l'original "body failed")
}
SuppressedError est ce qui rend using fiable dans du code sujet aux erreurs.
Support actuel : disponible, pas « à venir »
La gestion explicite des ressources est une proposition TC39 finalisée et standardisée dans ES2026 — pas une expérimentation en Stage 3 nécessitant une transpilation partout. À mi-2026, le tableau est le suivant :
| Environnement | Statut | Notes |
|---|---|---|
| Chrome / Edge / Opera | Disponible | using/await using depuis Chromium 134 (V8 13.8) ; le symbole Symbol.dispose seul est présent depuis Chrome 125 |
| Node.js | Disponible | using/await using natif dans Node.js 24 (V8 13.6) ; les symboles depuis la version 18.18.0 |
| Firefox | Disponible | Depuis Firefox 141 (version stable actuelle 152) |
| Safari | Partiel | Le symbole Symbol.dispose est disponible dans Safari desktop 26.4 ; Safari sur iOS ne le supporte pas encore |
| TypeScript | Disponible | Syntaxe using depuis la version 5.2 ; version stable actuelle 6.0 |
Un piège mérite d’être signalé : un symbole Symbol.dispose défini ne signifie pas que le moteur analyse using. Le symbole arrive régulièrement avant la déclaration — Node a exposé les symboles bien connus dès la ligne v18 (18.18.0) mais n’a rendu la syntaxe de déclaration native que dans Node 24, et Chrome a publié le symbole vers la v125 sans analyser using avant la version 134. Ainsi, sur un moteur plus ancien, vous pouvez référencer Symbol.dispose et obtenir quand même une erreur de syntaxe ou un TypeError « not disposable » — et un tableau de support de Symbol.dispose (comme celui lié ci-dessus) devance le support réel de using. Le support complet de using est une question de version du moteur, pas de polyfill.
Pour Safari et les cibles plus anciennes, transpilez. TypeScript nécessite de définir votre cible de compilation à ES2022 ou inférieure et de configurer lib pour inclure "esnext" ou "esnext.disposable" (TypeScript supporte la syntaxe depuis la version 5.2), et vous pouvez polyfiller les globaux avec core-js ou le paquet disposablestack.
Le code de nettoyage mécanique et sujet aux erreurs que vous écriviez manuellement dispose désormais d’une primitive du langage : ajoutez [Symbol.dispose] ou [Symbol.asyncDispose] à tout ce qui nécessite un nettoyage, déclarez-le avec using ou await using, et le moteur garantit le nettoyage dans le bon ordre sur chaque chemin de sortie. Commencez par envelopper la ressource que vous oubliez le plus souvent de fermer — une connexion, un verrou, un lecteur — et laissez la portée faire le reste.
FAQ
Quelle est la différence entre using et await using ?
Une déclaration using appelle la méthode synchrone Symbol.dispose de l'objet à la sortie du bloc, tandis que await using appelle Symbol.asyncDispose et l'attend, de sorte que le nettoyage renvoyant une promesse se termine avant la fermeture de la portée. Utilisez using lorsque le nettoyage est synchrone et await using lorsque le nettoyage renvoie une promesse. Comme await using accepte également les disposables synchrones ordinaires, vous pouvez l'utiliser chaque fois que vous n'êtes pas certain du type de nettoyeur que porte un objet.
Pourquoi using lève-t-il une erreur 'not disposable' sur Node 18 même si Symbol.dispose existe ?
Un symbole Symbol.dispose défini ne signifie pas que le moteur peut analyser la déclaration using. Node a exposé les symboles bien connus Symbol.dispose et Symbol.asyncDispose depuis la version 18.18.0, mais la syntaxe complète des déclarations using et await using n'est devenue native que dans Node 24, qui embarque V8 13.6. Sur les versions plus anciennes de Node, vous pouvez référencer le symbole et obtenir quand même une erreur de syntaxe ou un TypeError sur les handles natifs. Le support complet de using est une question de version du moteur, pas de polyfill.
La gestion explicite des ressources remplace-t-elle le garbage collection ?
Non. Le garbage collector récupère la mémoire selon son propre calendrier non déterministe et n'a jamais fermé de fichiers, libéré de verrous ni désabonné d'écouteurs d'événements. La gestion explicite des ressources traite ces ressources non-mémoire de manière déterministe, en exécutant leur nettoyage dès qu'une portée se termine. Les deux mécanismes répondent à des problèmes différents. WeakRef et FinalizationRegistry sont les outils liés au GC pour la mémoire, tandis que using et await using offrent un nettoyage délimité par portée pour les descripteurs de fichiers, les sockets, les verrous et les connexions.
Comment utiliser le mot-clé using avec une bibliothèque qui n'a pas de méthode dispose ?
Attachez le symbole vous-même avec Object.assign. Un client qui expose une méthode close ou end mais pas de symbole de nettoyage devient disposable en une seule ligne en assignant une méthode Symbol.asyncDispose asynchrone qui appelle la méthode close existante. Vous pouvez ensuite déclarer l'objet avec await using ou l'enregistrer avec un appel à AsyncDisposableStack use. Si vous passez ultérieurement à une version de la bibliothèque qui implémente le protocole nativement, la réassignation déclenchera une erreur vous rappelant de supprimer le shim.
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