JavaScript's "destructors" or the explicit resource management
					Published on 
					
Updated on 
				
 
	The limitations of the JavaScript's current resource management
JavaScript is a language with a garbage collection (GC). This means if any value becomes inaccessible it will be eventually deallocated from the memory. This implicit cleanup mechanism works well for a lot of cases and developers shouldn't worry about memory leaks as long as they don't leave references on no longer used resources.
However, this works well only if it's about deallocating the objects from the JS memory heap. Some resources, such as open file handles, also require additional custom logic for the resource deallocation, such as closing the open file handles. This is especially important in NodeJS.
Nowadays, in order to do this reliably, experienced developers usually write something like this:
let fileHandle;
try {
	fileHandle = await fs.open('some_file_path', 'r');
	// Do something
	// ...
} finally {
	// Wrapping in finally guarantees that the 
	// file handle will eventually close
	// even in a case of an error.
	filehandle?.close();
}
	Why doesn't JavaScript support destructors?
		As you see, this construct looks bulky. Also notice that fileHandle should be declared outside the
		try block in order to be accessible from the finally block. So why doesn't JavaScript
		support destructors which will cleanup the code eventually and
		allow us to avoid writing such boilerplate code?
		There are some possible reasons for this:
	
- It's now too late to interpret a function with a name "destructor" or any other name as a
			destructor, because it will
			break the backward compatibility. The only acceptable way is to add some symbol, such as
			Symbol.destructor.
- Even if we add a symbol for the destructor, the destructor will not be called immediately, since it takes
			time for the garbage collector to determine which objects are not reachable. This will not work very
			predictably and consistently across different JavaScript engines. Also, JavaScript already supports
			WeakRefandFinalizationRegistrywhich are not recommended to be used even by Mozilla because of the mentioned reason.
- Garbage collectors will have to somehow handle exceptions, infinite loops, stuck code and deadlocks in the destructor. This is unrealistic.
In short, forget about destructors in JavaScript.
Explicit resource management
		In order to solve this problem a proposal was created for explicit resource management.
		Basically, the feature adds keyword using and 2 new symbols: Symbol.dispose and
		Symbol.asyncDispose.
	
How using, Symbol.dispose and Symbol.asyncDispose work
Suppose you have some resource class called DisposableResource and you want the cleanup method to be
		called automatically when the resource variable is no longer in the block scope. The explicit resource
		management for the simple synchronous case can be done like this:
class DisposableResource {
	constructor() {
		console.log('The resource is created');
	}
	[Symbol.dispose]() {
		console.log('The resource is disposed');
	}
}
{
	// Keyword "using" behaves like "const", but also tells the compiler to dispose
	// the object immediately after the declared variable becomes unreachable.
	using resource = new DisposableResource();
	console.log('Doing something with the resource...');
}
// After the end of the resource block scope [Symbol.dispose]() is called.
	[Symbol.dispose]() will be called even when an uncaught error is thrown:
class DisposableResource {
	constructor() {
		console.log('The resource is created');
	}
	[Symbol.dispose]() {
		console.log('The resource is disposed');
	}
}
{
	using resource = new DisposableResource();
	throw "Error";
}
// [Symbol.dispose]() will be called even when an uncaught error occurs.
	The resources are disposed in the reverse order of their declaration to ensure that the older resources are available when disposing the newer resources which might depend on the older resources:
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
	}
}
{
	using resourceA = new DisposableResource('A');
	using resourceB = new DisposableResource('B');
	console.log('Doing something with the resources...');
}
// The resource B is disposed
// The resource A is disposed
	
		If an uncaught exception occurs during the disposal(s), SuppressedError is thrown.
	
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
		throw `Error occurred during the disposal of ${this.#name}`;
	}
}
{
	using resourceA = new DisposableResource('A');
	using resourceB = new DisposableResource('B');
}
// SuppressedError 
	Asynchronous disposal
		The disposal can also be asynchronous if it's used in an asynchronous function or the top level context of a JS
		module. In this
		case we need
		to use keyword await using and Symbol.asyncDispose:
	
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
	}
	async [Symbol.asyncDispose]() {
		console.log(`The resource ${this.#name} is disposed asynchronously`);
	}
}
{
	using resourceA = new DisposableResource('A');
	using resourceB = new DisposableResource('B');
	await using resourceC = new DisposableResource('C');
	await using resourceD = new DisposableResource('D');
	console.log('Doing something with the resources...');
}
// The resource D is disposed asynchronously
// The resource C is disposed asynchronously
// The resource B is disposed
// The resource A is disposed
	If [Symbol.asyncDispose] is missing, [Symbol.dispose] will be called instead:
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
	}
}
{
	using resourceA = new DisposableResource('A');
	using resourceB = new DisposableResource('B');
	await using resourceC = new DisposableResource('C');
	await using resourceD = new DisposableResource('D');
	console.log('Doing something with the resources...');
}
// The resource D is disposed
// The resource C is disposed
// The resource B is disposed
// The resource A is disposed
	Disposal in for loops
JavaScript also adds a nice syntax for using disposal for each item in the loop, here are examples for all combinations of sync/async loops and sync/async disposals:
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
	}
	async [Symbol.asyncDispose]() {
		console.log(`The resource ${this.#name} is disposed asynchronously`);
	}
}
function * syncGeneratorSyncDisposal() {
	yield new DisposableResource('A');
	yield new DisposableResource('B');
}
function * syncGeneratorAsyncDisposal() {
	yield new DisposableResource('C');
	yield new DisposableResource('D');
}
async function * asyncGeneratorSyncDisposal() {
	yield new DisposableResource('E');
	yield new DisposableResource('F');
}
async function * asyncGeneratorAsyncDisposal() {
	yield new DisposableResource('G');
	yield new DisposableResource('H');
}
for (using resource of syncGeneratorSyncDisposal()) {
	// Do something with the resource
}
// The resource A is created
// The resource A is disposed
// The resource B is created
// The resource B is disposed
for (await using resource of syncGeneratorAsyncDisposal()) {
	// Do something with the resource
}
// The resource C is created
// The resource C is disposed asynchronously
// The resource D is created
// The resource D is disposed asynchronously
for await (using resource of asyncGeneratorSyncDisposal()) {
	// Do something with the resource
}
// The resource E is created
// The resource E is disposed
// The resource F is created
// The resource F is disposed
for await (await using resource of asyncGeneratorAsyncDisposal()) {
	// Do something with the resource
}
// The resource G is created
// The resource G is disposed asynchronously
// The resource H is created
// The resource H is disposed asynchronously
	DisposableStack and AsyncDisposableStack
		As a bonus we have 2 built-in classes that help us to manage the resources: DisposableStack and
		AsyncDisposableStack. As you might guessed it, DisposableStack is for synchronous
		disposal and AsyncDisposableStack is for asynchronous disposal. They already have their
		respective [Symbol.dispose]() / [Symbol.asyncDispose]() methods. In addition to this,
		they have the following main methods and properties:
	
- disposed. A getter that returns a boolean indicating whether the stack has been disposed.
- dispose()(- DisposableStackonly). Disposes the stack.
- disposeAsync()(- AsyncDisposableStackonly). Disposes the stack asynchronously, returns a promise.
- use(value). Adds a disposable resource to the top of the stack. Has no effect if provided- nullor- undefined.
- adopt(value, onDispose). Adds a non disposable resource to the top of the stack and an associated callback. The callback will be called with the- valueas an argument when the stack is disposed.
- defer(onDispose). Adds a callback which is called after the stack is disposed.
- move(). Returns a new disposable stack with the added items and empties the current stack.
		DisposableStack and AsyncDisposableStack are useful for grouping the resources into a
		single object. When they're disposed they dispose the added resources in the reverse order.
	
Here are some examples:
class DisposableResource {
	#name;
	constructor(name) {
		this.#name = name;
		console.log(`The resource ${this.#name} is created`);
	}
	[Symbol.dispose]() {
		console.log(`The resource ${this.#name} is disposed`);
	}
	async [Symbol.asyncDispose]() {
		console.log(`The resource ${this.#name} is disposed asynchronously`);
	}
}
{
	using syncStack = new DisposableStack();
	await using asyncStack = new AsyncDisposableStack();
	syncStack.use(new DisposableResource('A'));
	syncStack.use(new DisposableResource('B'));
	asyncStack.use(new DisposableResource('C'));
	asyncStack.use(new DisposableResource('D'));
}
console.log('All resources are disposed');
// The resource A is created
// The resource B is created
// The resource C is created
// The resource D is created
// The resource D is disposed asynchronously
// The resource C is disposed asynchronously
// The resource B is disposed
// The resource A is disposed
// All resources are disposed
	Conclusion
Explicit resource management is a very nice feature to have, especially considering that JavaScript cannot support destructors. It allows to write clean and reliable code for resource cleanups. Currently this feature is not supported across all the mainstream browsers, so use it with caution. This feature is very useful for NodeJS because on the server side you often deal with resources that need some logic for cleanups, such as files and locks. Starting from NodeJS 24 you can use this feature with caution. Some APIs already have this experimental feature integrated, such as file handles. Chrome 134+ also support this feature. Since the feature is in stage 3, we will see it in other browsers soon.