[](https://badge.fury.io/js/resource-finalizer)
npm install resource-finalizer
Deterministic cleanup helpers for TypeScript/JavaScript based on ECMAScript Explicit Resource Management (using / await using) and DisposableStack.
The package provides two base classes:
- Destructor — synchronous cleanup
- AsyncDestructor — asynchronous cleanup
And two ready-to-use scope guards:
- ScopeGuard — run a callback on scope exit (sync)
- AsyncScopeGuard — run an async callback on scope exit (async)
When an instance is disposed, all destructors declared in the class inheritance chain are invoked automatically (from the most-derived class to the base class).
---
- ✅ Works with using / await using (and manual Symbol.dispose / Symbol.asyncDispose calls)
- ✅ Automatic destructor chaining across inheritance (C -> B -> A)
- ✅ Built-in DisposableStack / AsyncDisposableStack (via the disposablestack polyfill)
- ✅ Scope guards for ad-hoc cleanup (ScopeGuard / AsyncScopeGuard)
- ✅ Small API surface, TypeScript-first typings
---
``bash`
npm install resource-finalizer
---
- TypeScript: enable the disposable APIs in your tsconfig.json:
`jsonc`
{
"compilerOptions": {
"lib": ["ES2022", "ESNext.Disposable"]
}
}
- Runtime: this library imports disposablestack/auto internally to ensure DisposableStack / AsyncDisposableStack exist on globalThis.
> using / await using are part of the Explicit Resource Management proposal and require TypeScript (or a runtime) that understands this syntax.
---
`ts
import { Symbols, Destructor } from 'resource-finalizer';
class A extends Destructor {
public constructor() {
super();
console.log('[A] constructor');
}
public [Symbols.destructor](): void {
console.log('[A] destructor');
}
}
class B extends A {
public constructor() {
super();
console.log('[B] constructor');
}
public [Symbols.destructor](): void {
console.log('[B] destructor');
}
}
class C extends B {
public constructor() {
super();
console.log('[C] constructor');
}
public [Symbols.destructor](): void {
console.log('[C] destructor');
}
}
{
using instance = new C();
console.log('End scope');
}
console.log('End code');
`
Expected order:
- constructors: A -> B -> CC -> B -> A
- destructors (on scope exit):
---
`ts
import { Symbols, AsyncDestructor } from 'resource-finalizer';
class A extends AsyncDestructor {
public constructor() {
super();
console.log('[Async][A] constructor');
}
public async [Symbols.asyncDestructor](): Promise
console.log('[Async][A] destructor');
}
}
class B extends A {
public constructor() {
super();
console.log('[Async][B] constructor');
}
public async [Symbols.asyncDestructor](): Promise
console.log('[Async][B] destructor');
}
}
class C extends B {
public constructor() {
super();
console.log('[Async][C] constructor');
}
public async [Symbols.asyncDestructor](): Promise
console.log('[Async][C] destructor');
}
}
(async () => {
{
await using instance = new C();
console.log('[Async] End scope');
}
console.log('[Async] End code');
})().catch(console.error);
`
---
If you only need “run this cleanup when the scope ends”, you don’t have to define a new class.
Use ScopeGuard / AsyncScopeGuard — small wrappers around Destructor / AsyncDestructor that execute a user-provided finalizer when disposed.
`ts
import { ScopeGuard } from 'resource-finalizer';
{
using _ = new ScopeGuard(() => {
console.log('cleanup runs on scope exit');
});
console.log('work');
}
console.log('after scope');
`
`ts
import { AsyncScopeGuard } from 'resource-finalizer';
import { promises as fs } from 'node:fs';
async function demo() {
const path = './tmp.txt';
await fs.writeFile(path, 'hello');
await using _ = new AsyncScopeGuard(async () => {
await fs.rm(path, { force: true });
});
// use the file...
}
`
`ts
import { ScopeGuard, AsyncScopeGuard } from 'resource-finalizer';
const g = new ScopeGuard(() => console.log('cleanup'));
try {
// work...
} finally {
g[Symbol.dispose]();
}
async function demoAsync() {
const g = new AsyncScopeGuard(async () => console.log('async cleanup'));
try {
// work...
} finally {
await g[Symbol.asyncDispose]();
}
}
`
Because scope guards implement Disposable / AsyncDisposable, you can register them in a stack:
`ts
import { Symbols, Destructor, ScopeGuard } from 'resource-finalizer';
class Service extends Destructor {
public constructor() {
super();
this[Symbols.disposableStack].use(
new ScopeGuard(() => console.log('Service stopped'))
);
}
public [Symbols.destructor](): void {
// other cleanup...
}
}
`
---
`ts
import { Symbols, Destructible, createDisposableStack, callDestructorsChain } from 'resource-finalizer';
class SomeBaseClass {}
/**
* We need to add destructor support to a class that is already a derived class.
* To do this, you need to implement the following yourself
*
* - [Symbols.disposableStack]: DisposableStack;
* - [Symbols.callDestructorsChain](): void;
* - [Symbols.destructor](): void;
* - [Symbol.dispose](): void;
*/
class A extends SomeBaseClass implements Destructible {
public [Symbols.disposableStack] = createDisposableStack();
public [Symbol.dispose](): void {
this[Symbols.disposableStack].dispose();
}
public constructor() {
super();
this[Symbols.disposableStack].defer(() => {
this[Symbols.callDestructorsChain]();
});
console.log('[A] constructor');
}
public [Symbols.callDestructorsChain](): void {
callDestructorsChain(this);
}
public [Symbols.destructor](): void {
console.log('[A] destructor');
}
}
class B extends A {
public constructor() {
super();
console.log('[B] constructor');
}
public [Symbols.destructor](): void {
console.log('[B] destructor');
}
}
class C extends B {
public constructor() {
super();
console.log('[C] constructor');
}
public [Symbols.destructor](): void {
console.log('[C] destructor');
}
}
{
using instance = new C();
console.log('End scope');
}
console.log('End code');
`
---
Every Destructor instance owns a DisposableStack accessible via a symbol key:
`ts
import { Symbols, Destructor } from 'resource-finalizer';
class FileHandle extends Destructor {
private fd: number;
public constructor(fd: number) {
super();
this.fd = fd;
// Register cleanup actions.
this[Symbols.disposableStack].defer(() => {
// close(fd)
});
}
public [Symbols.destructor](): void {
// Additional destructor logic (logging, metrics, invariants, etc.)
}
}
`
For async cleanup, use AsyncDestructor and Symbols.asyncDisposableStack.
---
The destructor chain is discovered by walking the prototype chain and calling destructors that are defined directly on each prototype.
That means:
- ✅ Define destructors as class methods: public [Symbols.destructor](){...}this[Symbols.destructor] = () => {}
- ❌ Don’t assign destructors as instance fields (e.g. ), because they won’t be found by the chain walker.super[Symbols.destructor]()
- ❌ Don’t call manually — the base destructors are called automatically and you’d double-run them.
---
A holder of unique symbols used as keys:
- Symbols.destructorSymbols.asyncDestructor
- Symbols.disposableStack
- Symbols.asyncDisposableStack
- Symbols.callDestructorsChain
- Symbols.asyncCallDestructorsChain
-
- Implements Disposable ([Symbol.dispose]())DisposableStack
- Provides an instance at this[Symbols.disposableStack]public abstract [Symbols.destructor](): void
- Requires you to implement
- Implements AsyncDisposable ([Symbol.asyncDispose]())AsyncDisposableStack
- Provides an instance at this[Symbols.asyncDisposableStack]public abstract [Symbols.asyncDestructor](): Promise
- Requires you to implement
- Extends Destructornew ScopeGuard(() => void)
- Constructor: [Symbol.dispose]()
- Executes the finalizer on / using scope exit
- Extends AsyncDestructornew AsyncScopeGuard(() => Promise
- Constructor: [Symbol.asyncDispose]()
- Executes the finalizer on / await using scope exit
- DestructibleAsyncDestructible
-
- DisposableStackAsyncDisposableStack
-
- createDisposableStack(): DisposableStackcreateAsyncDisposableStack(): AsyncDisposableStack
- callDestructorsChain(obj: object): void
- asyncCallDestructorsChain(obj: object): Promise
-
MIT