A proposal to extend Explicit Resource Management to add an opt-in strict using
usage requirement for resources via
Symbol.enter
.
This is a follow-on proposal originally documented in tc39/proposal-explicit-resource-management#195. It's purpose is to add an opt-in mechanism to enforce a stricter
resource management model that forces a user to use using
, await using
, DisposableStack
, and
AsyncDisposableStack
for resources.
Stage: 1
Champion: Ron Buckton (@rbuckton)
For more information see the TC39 proposal process.
- Ron Buckton (@rbuckton)
The current model for resource management in the Explicit Resource Management proposal allows for the creation and
use of Disposables — objects with a [Symbol.dispose]()
method whose lifetime is bound to the containing block
scope in tandem with a using
declaration.
One of the caveats to the current proposal is that there is no way to enforce that a Disposable has been assigned to
a using
declaration, or attached to a DisposableStack
object. As a result, users are free to construct a Disposable
without registration and neglect to perform cleanup manually.
A mechanism of enforcement is not included in the main proposal in an effort to ease adoption. Initially, it is expected
that the largest source of disposables is likely to come from host APIs, such as those in the DOM, NodeJS, or Electron.
These hosts already have existing
APIs that
can easily be adapted to support [Symbol.dispose]()
or [Symbol.asyncDispose]()
. Were such enforcement to be
mandatory, these hosts would need to implement and maintain a completely parallel set of APIs purely to support using
.
As a result, we are proposing this as a purely opt-in mechanism that could be leveraged by new APIs that need stronger
enforcement guarantees.
We propose the adoption of an additional resource management symbol — Symbol.enter
— along with additional
semantics for using
, await using
, DisposableStack.prototype.use
, and AsyncDisposableStack.prototype.use
.
When a resource is declared via a using
or await using
declaration, we would first inspect the resource for a
[Symbol.enter]()
method. If such a method is present, it would be invoked (synchronously, in both cases) and its
result would become the actual value that is bound and whose [Symbol.dispose]()
(or [Symbol.asyncDispose]()
) method
is registered.
function getStrictResource() {
// Construct the actual resource.
const actualResource = {
value: "resource",
[Symbol.dispose]() { }
};
// Construct and return a resource wrapper that imposes strict enforcement semantics.
return {
[Symbol.enter]() {
return actualResource;
}
};
}
{
using res = getStrictResource(); // retval[Symbol.enter]() is invoked.
res.value; // "resource"
} // res[Symbol.dispose]() is invoked.
This proposal makes no distinction between using
and await using
for the purpose of [Symbol.enter]()
, as a
[Symbol.enter]()
method may not be asynchronous. It's function is purely an enforcement guard and is not intended as
a means to delay initialization.
Both DisposableStack.prototype.use()
and AsyncDisposableStack.prototype.use()
function in a similar fashion to their
syntactic counterparts. If the resource provided has a [Symbol.enter]()
method, it is invoked and its result is the
actual resource that is registered and returned from the use()
method.
Symbol.enter
achieves strict enforcement by introducing an additional step between resource acquisition and use. The
strict enforcement wrapper object does not implement any resource-specific capabilities itself. Instead, it guides the
user towards using
as a more convenient alternative to manually unwrapping by directly invoking [Symbol.enter]()
.
If the Explicit Resource Management proposal were to ship with strict enforcement as the only option, hosts would
need to either bifurcate their API surface to introduce new, strictly enforced versions of existing APIs, or add an
additional [Symbol.enter]() { return this; }
to their existing APIs to conform.
We believe the most likely driver for adoption of using
will be as a direct result of adoption in host APIs. We expect
hosts are more likely to chose an approach that provides a path of least resistance for existing developers in an effort
to ease adoption, and thus would expect [Symbol.enter]() { return this; }
to be the approach that offers the least
resistance.
Many host APIs already guard native resources with a finalizer that is executed during garbage collection. For example,
fs.promises.FileHandle
in NodeJS will automatically release its underlying file handle if the FileHandle
instance is
garbage collected. In counterpoint, many user produced disposables are likely either to only contain memory-managed
objects that will already be freed by GC, to be composed of native resources provided by a host API, or a combination
thereof. Due to these factors, strict enforcement is not entirely necessary.
As a result, this proposal introduces Symbol.enter
as a purely opt-in mechanism. When not present on an object, the
runtime acts as if an implicit [Symbol.enter]() { return this; }
already exists. This aligns with the "path of least
resistance" approach hosts likely would have taken for existing APIs without the additional overhead of such a method.
It also allows a library or package author the autonomy to require strict enforcement if their API requires it.
Without Symbol.enter
, there are two alternatives that API authors can consider as a potential workaround for strict
enforcement:
- Leverage a
FinalizationRegistry
to issue warnings to the console when a resource is garbage collected rather than disposed. - Define
[Symbol.dispose]
as a getter rather than a method.
In this approach, we use a FinalizationRegistry
to warn users about improper cleanup of resources.
const registry = new FinalizationRegistry(() => {
console.warn("Resource was garbage collected without being disposed. Did you forget a 'using'?");
});
class Resource {
...
constructor() {
registry.register(this, undefined, this);
...
}
[Symbol.dispose]() {
registry.unregister(this);
...
}
}
Here, allocation of Resource
adds an entry to the finalization registry using itself as the unregister token. When the
[Symbol.dispose]()
method is invoked, the Resource
is removed from the finalization registry. If the
[Symbol.dispose]()
method is not invoked prior to garbage collection, the finalization registry's cleanup callback is
triggered issuing a warning to the console.
In this approach, we define [Symbol.dispose]
as a getter rather than a method. This allows us to guard against
a resource being used in an invalid state with the assumption that a direct access to [Symbol.dispose]
is enough of an
indication that resource registration has occurred.
class Resource {
#state = "unregistered"; // one of: "unregistered", "registered", or "disposed"
...
resourceOperation() {
this.#throwIfInvalid();
...
}
get [Symbol.dispose]() {
if (this.#state === "unregistered") {
this.#state = "registered";
}
return Resource.#dispose;
}
static #dispose = function() {
if (this.#state === "disposed") {
return;
}
this.#state = "disposed";
...
};
#throwIfInvalid() {
switch (this.#disposeState) {
case "unregistered":
throw new TypeError("This resource must either be registered via 'using' or attached to a 'DisposableStack'");
case "disposed":
throw new ReferenceError("Object is disposed");
}
}
}
Here, Resource
is initially constructed in an "unregistered"
state. Attempts to perform operations against the
resource are guarded by a call to #throwIfInvalid()
which checks for both whether the resource has been registered or
whether the resource has already been disposed. Since [Symbol.dispose]
is a getter, accessing the getter triggers a
state change from "unregistered"
to "registered"
and returns the actual dispose method that will be invoked by
using
at the end of the containing block.
We are explicitly not proposing a feature with the breadth of Python's Context Managers. Context managers allow you to intercept, replace, and even drop exceptions thrown within the context which is a far more complex mechanism than what is proposed. For further discussion on full exception handling, please refer to tc39/proposal-explicit-resource-management#49.
- Python Context Managers and
__enter__
.
CreateDisposableResource ( V, hint, method ) would be modified as follows:
1. If _method_ is not present, then
1. If _V_ is either *null* or *undefined*, then
1. Set _V_ to *undefined*.
1. Set _method_ to *undefined*.
1. Else,
1. If _V_ is not an Object, throw a *TypeError* exception.
+ 1. Let _enter_ be ? GetMethod(_V_, @@enter).
+ 1. If _enter_ is not *undefined*, then
+ 1. Set _V_ to ? Call(_enter_, _V_).
+ 1. If _V_ is not an Object, throw a *TypeError* exception.
1. Set _method_ to ? GetDisposeMethod(_V_, _hint_).
1. If _method_ is *undefined*, throw a *TypeError* exception.
1. Else,
1. If IsCallable(_method_) is *false*, throw a *TypeError* exception.
1. Return the DisposableResource Record { [[ResourceValue]]: _V_, [[Hint]]: _hint_, [[DisposeMethod]]: _method_ }.
This proposal would introduce the built-in symbol @@enter
, accessible as a static property named enter
on the
Symbol
constructor.
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- Identified a "champion" who will advance the addition.
- Prose outlining the problem or need and the general shape of a solution.
- Illustrative examples of usage.
- High-level API.
- Initial specification text.
- Transpiler support (Optional).
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
- Test262 acceptance tests have been written for mainline usage scenarios and merged.
- Two compatible implementations which pass the acceptance tests: [1], [2].
- A pull request has been sent to tc39/ecma262 with the integrated spec text.
- The ECMAScript editor has signed off on the pull request.