Back Original

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: () => {
            
            return Date.now() - runtimeStart > 100;
        },
    });
    const ctx = runtime.newContext();

    
    const accessLog: string[] = [];

    
    
    
    const locationHandle = ctx.newObject();
    ctx.setProp(ctx.global, 'location', locationHandle);
    ctx.defineProp(locationHandle, 'search', {
        configurable: false,
        enumerable: true,
        get: () => {
            
            accessLog.push('search');
            
            return ctx.newString(window.location.search);
        },
    });

    
    const result = ctx.evalCode(code, 'untrusted.js');

    if (result.error) {
        
        result.error.dispose();
        locationHandle.dispose();
        ctx.dispose();
        runtime.dispose();

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

    
    const evaluationResult = ctx.dump(result.unwrap());

    
    result.dispose();
    locationHandle.dispose();
    ctx.dispose();
    runtime.dispose();

    
    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();

    
    using runtime = QuickJS.newRuntime({
        interruptHandler: () => {
            
            return Date.now() - runtimeStart > 100;
        },
    });
    using ctx = runtime.newContext();

    
    const accessLog: string[] = [];

    
    
    
    using locationHandle = ctx.newObject();
    ctx.setProp(ctx.global, 'location', locationHandle);
    ctx.defineProp(locationHandle, 'search', {
        configurable: false,
        enumerable: true,
        get: () => {
            
            accessLog.push('search');
            
            return ctx.newString(window.location.search);
        },
    });

    
    using result = ctx.evalCode(code, 'untrusted.js');

    if (result.error) {
        
        throw new Error('Error executing code');
    }

    
    const evaluationResult = ctx.dump(result.unwrap());

    
    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:

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';


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 {
  

  resource1: SomeResource;
  resource2: SomeOtherResource;

  constructor() {
    
    this.resource1 = allocateSomeResource();
    this.resource2 = allocateSomeOtherResource();
  }

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

function doWork() {
  using manager = new ResourceManager();
  
  return; 
}

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