JavaScript Explicit Resource Management: using and Symbol.dispose

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.