JavaScript's upcoming Explicit Resource Management is great!

I've been using the quickjs-emscripten package a bit recently. It's a Web Assembly compiled wrapper around Fabrice Bellard's QuickJS, a small & modern JavaScript engine.

This allows me to execute sandboxed JavaScript in an environment that is completely controlled!

For example, here's some code showing how arbitrary JavaScript code can be evaluated safely. It isolates the code so it doesn't have access to the outside environment with the exception of a special location.search global variable, which reflects the browser's query string as a readonly value.

It also tracks reads to that query string in an accessLog:

safeEval.ts
import { getQuickJS } from 'quickjs-emscripten';

async function safeEval(code: string) {
    const QuickJS = await getQuickJS();
    const runtimeStart = Date.now();
    const runtime = QuickJS.newRuntime({
        interruptHandler: () => {
            // โฐ Abort if execution lasts more than 100ms
            return Date.now() - runtimeStart > 100;
        },
    });
    const ctx = runtime.newContext();

    // ๐Ÿ‘€ Keep track of any access to the location
    const accessLog: string[] = [];

    // ๐Ÿค– Simulate in the sandbox:
    //    var location = {}
    //    Object.defineProperty(location, 'search', { ... });
    const locationHandle = ctx.newObject();
    ctx.setProp(ctx.global, 'location', locationHandle);
    ctx.defineProp(locationHandle, 'search', {
        configurable: false,
        enumerable: true,
        get: () => {
            // Sandbox code evaluated location.search!
            accessLog.push('search');
            // Return a string value that can be understood by the sandbox
            return ctx.newString(window.location.search);
        },
    });

    // ๐Ÿฆพ Safely evaluate the untrusted code
    const result = ctx.evalCode(code, 'untrusted.js');

    if (result.error) {
        // ๐Ÿ˜ฑ Something bad happened, clean up
        result.error.dispose();
        locationHandle.dispose();
        ctx.dispose();
        runtime.dispose();

        throw new Error('Error executing code');
    }

    // ๐Ÿ”€ Convert the evaluated sandbox value to something we can read
    const evaluationResult = ctx.dump(result.unwrap());

    // ๐Ÿงน Clean up
    result.dispose();
    locationHandle.dispose();
    ctx.dispose();
    runtime.dispose();

    // โœจ And we're done!
    return { evaluationResult, accessLog };
}

While this works great, it's a bit awkward to use because it requires explicit disposal of "handle" objects that reference values inside of the QuickJS engine.

See all those .dispose() calls? If one is missed, it causes a memory leak.

And to make matters worse, sometimes you need to exit early, or catch exceptions, or do all sorts of things which make it complicated to keep track of all of the objects that need to be .dispose()d.

No need to fret, Explicit Resource Management makes this much easier!

Explicit Resource Management is an ECMAScript language proposal that introduces a new keyword using, which is similar to const, but is used to define resources that need to be disposed when they leave scope.

It means that the above code could be rewritten as:

safeEval.ts
import { getQuickJS } from 'quickjs-emscripten';

async function safeEval(code: string) {
    const QuickJS = await getQuickJS();
    const runtimeStart = Date.now();

    // โœจ New syntax: declare a auto-disposed resource
    using runtime = QuickJS.newRuntime({
        interruptHandler: () => {
            // โฐ Abort if execution lasts more than 100ms
            return Date.now() - runtimeStart > 100;
        },
    });
    using ctx = runtime.newContext();

    // ๐Ÿ‘€ Keep track of any access to the location
    const accessLog: string[] = [];

    // ๐Ÿค– Simulate in the sandbox:
    //    var location = {}
    //    Object.defineProperty(location, 'search', { ... });
    using locationHandle = ctx.newObject();
    ctx.setProp(ctx.global, 'location', locationHandle);
    ctx.defineProp(locationHandle, 'search', {
        configurable: false,
        enumerable: true,
        get: () => {
            // Sandbox code evaluated location.search!
            accessLog.push('search');
            // Return a string value that can be understood by the sandbox
            return ctx.newString(window.location.search);
        },
    });

    // ๐Ÿฆพ Safely evaluate the untrusted code
    using result = ctx.evalCode(code, 'untrusted.js');

    if (result.error) {
        // ๐Ÿ˜ฑ Something bad happened!
        throw new Error('Error executing code');
    }

    // ๐Ÿ”€ Convert the evaluated sandbox value to something we can read
    const evaluationResult = ctx.dump(result.unwrap());

    // โœจ And we're done!
    return { evaluationResult, accessLog };
}

See that? No more .dispose() calls!

Regardless of how scope is left (here, variables may go out of scope when an exception is thrown or we return), the objects are automatically disposed when execution leaves the syntactic scope.

And because this feature has reached Stage 3, it's both very likely to be coming soon to the standard language and can be used today if you build your JavaScript.

Using it today

I use TypeScript, prettier, eslint, esbuild, vite, and neovim to develop and build my code. And all of them fully support this proposal and esbuild can produce code that performs disposal correctly in JS engines that haven't yet implemented the functionality.

Here's how to get them working:

TypeScript:

You must use TypeScript 5.2 or above.

Your tsconfig.json must have:

  • compilerOptions.lib containing "ES2019" and "esnext.disposable"
  • compilerOptions.target set to something lower than "esnext", I use "ES2019" since 5 years old is a reasonable target for my projects.

My tsconfig.json looks like this:

tsconfig.json
{
  "compilerOptions": {
    "lib": ["ES2019", "esnext.disposable", "DOM"],
    "target": "ES2019",
  },
  // ...
}

prettier:

Supported out of the box in prettier version 3.0.3 or later.

eslint:

I use typescript-eslint/parser, which got support in typescript-eslint version 7.13.1.

esbuild:

Implemented in version 0.18.17, bugfixes in version 0.18.19.

You must specify a target of something lower than esnext. Same with TypeScript, I use es2019.

vite:

Since vite uses esbuild under the hood to bundle, you'll need to set a target of es2019 (or whatever works best for you) when building:

vite.config.js
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
    base: './',
    build: {
        assetsDir: './',
        target: 'es2019',
    },
    esbuild: {
        target: 'es2019',
    },
});

vim/neovim:

To get the using keyword working correctly, you need to tell vim/neovim that using is a keyword.

There's probably a better way to do this, but what I ended up doing was adding two files (~/.config/nvim/after/syntax/javascript.vim and ~/.config/nvim/after/syntax/typescript.vim) with the same contents:

javascript.vim
syn keyword esNextKeyword using
hi def link esNextKeyword Keyword

This tells vim that after loading the syntax files for javascript/typescript (and javascriptreact/typescriptreact), it should treat using as a new keyword in a class named esNextKeyword. And link that new class to the Keyword definition.

Note: Stage 3 is not a guarantee that it will ship as-is in browsers and other JavaScript engines.

I personally think it will ship, since it will lead to standardizing a well-known pattern (RAII) found in other languages.

Resources holding resources

The nice thing about this proposal is that it's just enough of a change to allow for new patterns to emerge.

Say you've got a class which owns and manages a set of resources. The problem is now that class instances needs to dispose of its resources when it's no longer used.

Now you can just add a Symbol.dispose method on your class, and it can be disposed in the same way as the resources it manages.

This pattern looks like this:

class ResourceManager implements Disposable {
  // New interface!              ^^^^^^^^^^

  resource1: SomeResource;
  resource2: SomeOtherResource;

  constructor() {
    // "Own" some resources that should be disposed when this instance is disposed
    this.resource1 = allocateSomeResource();
    this.resource2 = allocateSomeOtherResource();
  }

  [Symbol.dispose]() {
    this.resource1[Symbol.dispose]();
    this.resource2[Symbol.dispose]();
  }
}

function doWork() {
  using manager = new ResourceManager();
  // do stuff
  return; // โœจ Owned resources automatically disposed!
}

This lets us do RAII in JavaScript easily, which can help avoid resource leaks and better standardize the structure of code that manages resources.