ES2025 finalises the Explicit Resource Management proposal — the using declaration and Symbol.dispose / Symbol.asyncDispose protocol. using works like a try/finally block but at the variable declaration level: when the variable goes out of scope (or an exception is thrown), the object’s [Symbol.dispose]() method is called automatically. This eliminates entire classes of resource-leak bugs in WordPress admin scripts that open connections, lock files, or register event listeners.
Problem: JavaScript code that acquires resources — file handles, database connections, event listeners, WebSocket sessions — must clean them up in finally blocks or via explicit dispose() calls, which are easy to forget, especially when exceptions occur.
Solution: Use Explicit Resource Management (using keyword, ES2025) — declare a resource with using resource = acquire(), and the object's [Symbol.dispose]() method is automatically called when the block exits, whether normally or via exception. Use await using for async cleanup with [Symbol.asyncDispose]().
The examples below show basic using syntax, adding Symbol.dispose to an existing class, async disposal with await using, and a WordPress admin pattern for a fetch controller that is automatically cancelled when the enclosing function exits.
// ── 1. Basic using declaration ────────────────────────────────────────────
class TempElement {
constructor( tag ) {
this.el = document.createElement( tag );
document.body.appendChild( this.el );
}
[Symbol.dispose]() {
this.el.remove(); // auto-cleanup when 'using' scope exits
console.log( 'Element removed' );
}
}
function renderTempUI() {
using div = new TempElement( 'div' );
div.el.textContent = 'Loading…';
// div.el is visible here
// When renderTempUI() returns, [Symbol.dispose]() is called automatically
}
// ── 2. Auto-cancelling fetch with AbortController ─────────────────────────
class ManagedFetch {
constructor() {
this.controller = new AbortController();
}
get signal() { return this.controller.signal; }
[Symbol.dispose]() {
this.controller.abort( 'scope exited' );
}
}
async function loadPosts( url ) {
using fetchCtrl = new ManagedFetch();
const response = await fetch( url, { signal: fetchCtrl.signal } );
// If loadPosts() throws before this line, the fetch is aborted automatically
return response.json();
// fetchCtrl.controller.abort() called automatically when function returns
}
// ── 3. Async disposal with await using ───────────────────────────────────
class DBConnection {
constructor( url ) { this.url = url; }
async connect() {
// simulate opening connection
this.conn = await openConnection( this.url );
}
async [Symbol.asyncDispose]() {
await this.conn?.close();
console.log( 'Connection closed' );
}
}
async function runQuery( sql ) {
await using db = new DBConnection( 'mysql://localhost/wp' );
await db.connect();
return db.conn.query( sql );
// db[Symbol.asyncDispose]() is awaited automatically on scope exit
}
// ── 4. WordPress admin: auto-remove event listeners on scope exit ─────────
class EventSubscription {
constructor( el, event, handler ) {
this.el = el;
this.event = event;
this.handler = handler;
el.addEventListener( event, handler );
}
[Symbol.dispose]() {
this.el.removeEventListener( this.event, this.handler );
}
}
function setupTempListener() {
const handler = e => console.log( 'clicked', e.target );
using sub = new EventSubscription( document.body, 'click', handler );
// … do work …
// listener removed automatically when setupTempListener returns
}
NOTE: using guarantees disposal even when an exception is thrown — it is equivalent to a try/finally block; if both the body and the dispose method throw, the body's error is suppressed and the disposal error is thrown; use SuppressedError (also new in this proposal) to inspect both errors if needed.