ECMAScript Explicit Resource Management early implementation in Typescript 5.2

Mohi Bagherani
4 min readAug 24, 2023

--

Explicit Resource Management indicates a system whereby the lifetime of a “resource” is managed explicitly by the user either imperatively (by directly calling a method like Symbol.dispose) or declaratively (through a block-scoped declaration like using).

A Resource is an object with a specific lifetime, at the end of which either a lifetime-sensitive operation should be performed or a non-garbage-collected reference (such as a file handle, socket, etc.) should be closed or freed.

At the moment of writing this article, the ECMAScript proposal for Explicit Resource Management is at stage 3 which means it will be launched within a few months and this is the time that the TypeScript community has already implemented it in Typescript version 5.2.

This implementation will add a feature to the javascript language that we had similar to in other languages such as C# using syntax, Python with syntax, or try in Java.

// C# example of the 'using' statement
using (var file = File.OpenRead(“path-to-file”))
{
// use the file
}
// file.dispose() method has been called now to release resources.

In general, without using this new feature, a conventional free-up pattern is by using the try/finally syntax:

var obj;
try{
obj = someResource();
// ...
}
finally{
obj.release(); // or any other clean-up method provided by the resource
}

Now this can be written in this way in Typescript 5.2:

using obj = someResource();
// ...

Later when the execution of the program goes in outside scope of the place the obj has been defined, the dispose method will be called.

To tell the Typescript compiler how to translate the new using syntax into a try/finally that the current Javascript can interpret, Typescript came up with an interface called Disposable that can be implemented by your class that makes you to have a [Symbol.dispose] method in that class like below:

class MyResource implements Disposable {
[Symbol.dispose]() {
// clean-up logic
}
}

using obj = new MyResource();
// ...

Or if you have a function instead of a class, your function should return an object that has the Symbol.dispose method:

function myResource(): Disposable {
return {
[Symbol.dispose](){ /* clean-up logic */}
}
}

using obj = myResource();

Good to know that after compilation, the above code will become something like this:

class MyResource {
[Symbol.dispose]() {
// clean-up logic
}
}
var obj;
const env_1 = { stack: [], error: void 0, hasError: false };
try {
obj = __addDisposableResource(env_1, MyResource(), false);
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}

As you can see, here the Typescript compiler has compiled the using syntax into the traditional try/finally pattern that I mentioned before and of course, with some simple functions to manage the allocated resources(__addDisposableResource and __disposeResources will be generated by the compiler in the output file).

Nested scopes and using

Objects can be created in the nested scopes:

function work() {
using a = resource();
{
using b = resource();
}
}

work();
// b.dispose()
// a.dispose()

In this case, after leaving the scope, the dispose methods will be called respectively.

DisposableStack class

Implementing the Symbol.dispose can be considered a good pattern if you have a class or a function that needs to be disposed at someplace, but here are two problems: First, in some cases implementing this method might add too much abstraction to your code and second, there are a lot of libraries and modules that haven’t implemented the dispose method it yet. So, for using them, you have to wrap them up with another layer of abstraction and implement the clean-up code(dispose) by yourself.

Here is an example from the Typescript blog:

class TempFile implements Disposable {
#path: string;
#handle: number;

constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}

// other methods

[Symbol.dispose]() {
// Close the file and delete it.
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}

function doSomeWork() {
using file = new TempFile("path-to-file");
}

This class has been implemented (with all of its sophistications that might have later) to only have a dispose method that would be called by the engine, but there is a simpler solution that Typescript comes up with the DisposableStack class.

Here is how:

function doSomeWork() {
const path = "path-to-file";
const file = fs.openSync(path, "w+");

using cleanup = new DisposableStack();
cleanup.defer(() => {
fs.closeSync(file);
fs.unlinkSync(path);
});

// use file...
}

Here we’ve created an instance of DisposableStack right after opening the file; It has the defer method that accepts a callback function which is suitable for clean-up purpose that will be invoked at the moment of disposing the cleanup object.

After the above code is compiled, the resulting JavaScript code will include a finally block. This finally block will call the callback function that was passed to the defer method. This ensures that the callback function is always executed, even if an error occurs in the try block.

Async dispose method

Sometimes for your dispose logic you might need to use asynchronous operations using async/await. In this case you can simply use the await before the using keyword:

await using file = OpenFile('...');

In this case in the OpenFile function or class you should implement the dispose method using Symbol.asyncDispose like below:

import * as fs from "fs";

function OpenFile(path) : AsyncDisposable {
const file = fs.open(path);

return {
file,
async [Symbol.asyncDispose]() {
await fs.anAsyncOperation();
},
}
}

Polyfills

Because this feature is so recent, most runtimes will not support it natively. To use it, you will need runtime polyfills for the following:

  • Symbol.dispose
  • Symbol.asyncDispose
  • DisposableStack
  • AsyncDisposableStack
  • SuppressedError

Symbol.dispose and Symbol.asyncDispose can be polyfilled like this:

Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");

The compilation target in the tsconfig file should be es2022 or below and lib setting to either include “esnext” or “esnext.disposable”

{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.disposable", "dom"]
}
}

For more information on this feature, take a look at the work on GitHub!

--

--

Mohi Bagherani

Software Engineer, Web Developer and currently self-studying Machine Learning