Repository: nodejs/node
Pull request: meta: add guidelines for introduction of ERM support

Stemming from discussions in https://github.com/nodejs/node/pull/58516 , this seeks to add guidance for introducing explicit resource management support into existing Node.js APIs. This is mean to be discussed and evolved so please weigh in.

/cc @nodejs/tsc @nodejs/collaborators  @bakkot 

Changed files:
filename: doc/contributing/erm-guidelines.md
patch: @@ -0,0 +1,509 @@
+# Explicit Resource Management (`using`) Guidelines
+
+[Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)
+is a capability that was introduced to the JavaScript language in 2025. It provides a way
+of marking objects as disposable resources such that the JavaScript engine will automatically
+invoke disposal methods when the object is no longer in scope. For example:
+
+```js
+class MyResource {
+  dispose() {
+    console.log('Resource disposed');
+  }
+
+  [Symbol.dispose]() {
+    this.dispose();
+  }
+}
+
+{
+  using resource = new MyResource();
+  // When this block exits, the `Symbol.dispose` method will be called
+  // automatically by the JavaScript engine.
+}
+```
+
+This document outlines some specific guidelines for using explicit resource
+management in the Node.js project -- specifically, guidelines around how to
+make objects disposable and how to introduce the new capabilities into existing
+APIs.
+
+The caveat to this guidance is that explicit resource management is a brand new
+language feature, and there is not an existing body of experience to draw from
+when writing these guidelines. The points outlined here are based on the
+current understanding of how the mechanism works and how it is expected to
+be used. As such, these guidelines may change over time as more experience
+is gained with explicit resource management in Node.js and the ecosystem.
+It is always a good idea to check the latest version of this document, and
+more importantly, to suggest changes to it based on evolving understanding,
+needs, and experience.
+
+## Some background
+
+Objects can be made disposable by implementing either, or both, the
+`Symbol.dispose` and `Symbol.asyncDispose` methods:
+
+```js
+class MySyncResource {
+  [Symbol.dispose]() {
+    // Synchronous disposal logic
+  }
+}
+
+class MyAsyncDisposableResource {
+  async [Symbol.asyncDispose]() {
+    // Asynchronous disposal logic
+  }
+}
+```
+
+An object that implements `Symbol.dispose` can be used with the `using`
+statement, which will automatically call the `Symbol.dispose` method when the
+object goes out of scope. If an object implements `Symbol.asyncDispose`, it can
+be used with the `await using` statement in an asynchronous context. It is
+worth noting here that `await using` means the disposal is asynchronous,
+not the initialization.
+
+```mjs
+{
+  using resource = new MyResource();
+  await using asyncResource = new MyResource();
+}
+```
+
+Importantly, it is necessary to understand that the design of `using` makes it
+possible for user code to call the `Symbol.dispose` or `Symbol.asyncDispose`
+methods directly, outside of the `using` or `await using` statements. These
+can also be called multiple times and by any code that is holding a reference
+to the object. That is to say, explicit resource management does not imply
+ownership of the object. It is not a form of RAII (Resource Acquisition Is
+Initialization) as seen in some other languages and there is no notion of
+exclusive ownership of the object. A disposable object can become disposed
+at any time.
+
+The `Symbol.dispose` and `Symbol.asyncDispose` methods are called in both
+successful and exceptional exits from the scopes in which the `using` keyword
+is used. This means that if an exception is thrown within the scope, the
+disposal methods will still be called (similar to how `finally { }` blocks work).
+However, when the disposal methods are called they are not aware of the context.
+These methods will not receive any information about any exception that may have
+been thrown. This means that it is often safest to assume that the disposal
+methods will be called in a context where the object may not be in a valid
+state or that an exception may be pending.
+
+## Guidelines for disposable objects
+
+So with this in mind, it is necessary to outline some guidelines for disposers:
+
+1. Disposers should be idempotent. Multiple calls to the disposal methods
+   should not cause any issues or have any additional side effects.
+2. Disposers should assume that they are being called in an exception context.
+   Always assume there is likely a pending exception and that if the object
+   has not been explicitly closed when the disposal method is called, the
+   object should be disposed as if an exception had occurred. For instance,
+   if the object API exposes both a `close()` method and an `abort()` method,
+   the disposal method should call `abort()` if the object is not already
+   closed. If there is no difference in disposing in success or exception
+   contexts, then separate disposal methods are unnecessary.
+3. It is recommended to avoid throwing errors within disposers.
+   If a disposer throws an exception while there is another pending
+   exception, then both exceptions will be wrapped in a `SuppressedError`
+   that masks both. This makes it difficult to understand the context
+   in which the exceptions were thrown.
+4. Disposable objects should expose named disposal methods in addition
+   to the `Symbol.dispose` and `Symbol.asyncDispose` methods. This allows
+   user code to explicitly dispose of the object without using the `using`
+   or `await using` statements. For example, a disposable object might
+   expose a `close()` method that can be called to dispose of the object.
+   The `Symbol.dispose` and `Symbol.asyncDispose` methods should then invoke
+   these named disposal methods in an idempotent manner.
+5. Because it is safest to assume that the disposal method will be called
+   in an exception context, it is generally recommended to prefer use of
+   `Symbol.dispose` over `Symbol.asyncDispose` when possible. Asynchronous
+   disposal can lead to delaying the handling of exceptions and can make it
+   difficult to reason about the state of the object while the disposal is
+   in progress. Disposal in an exception context is preferably synchronous
+   and immediate. That said, for some types of objects async disposal is not
+   avoidable.
+6. Asynchronous disposers, by definition, are able to yield to other tasks
+   while waiting for their disposal task(s) to complete. This means that, as a
+   minimum, a `Symbol.asyncDispose` method must be an `async` function, and
+   must `await` at least one asynchronous disposal task. If either of these
+   criteria is not met, then the disposer is actually a synchronous disposer in
+   disguise, and will block the execution thread until it returns; such a
+   disposer should instead be defined using `Symbol.dispose`.
+7. Because the disposal process is strictly ordered, there is an intrinsic
+   expectation that all tasks performed by a single disposer are fully complete
+   at the point that the disposer returns. This means, for example, that
+   "callback-style" APIs must not be invoked within a disposer, unless they are
+   promisified and awaited. Any Promise created within a disposer must be
+   awaited, to ensure its resolution prior to the disposer returning.
+8. Avoid, as much as possible, using both `Symbol.dispose` and `Symbol.asyncDispose`
+   in the same object. This can make it difficult to reason about which method
+   will be called in a given context and could lead to unexpected behavior or
+   subtle bugs. This is not a firm rule, however; there may be specific cases
+   where it makes sense to define both, such as where a resource already exposes
+   both synchronous and asynchronous methods for closing down the resource.
+
+### Example disposable objects
+
+A disposable object can be quite simple:
+
+```js
+class MyResource {
+  #disposed = false;
+  dispose() {
+    if (this.#disposed) return;
+    this.#disposed = true;
+    console.log('Resource disposed');
+  }
+
+  [Symbol.dispose]() {
+    this.dispose();
+  }
+}
+
+{ using myDisposable = new MyResource(); }
+```
+
+Or even fully anonymous objects:
+
+```js
+function getDisposable() {
+  let disposed = false;
+  return {
+    dispose() {
+      if (disposed) return;
+      disposed = true;
+      console.log('Resource disposed');
+    },
+    [Symbol.dispose]() {
+      this.dispose();
+    },
+  };
+}
+
+{ using myDisposable = getDisposable(); }
+```
+
+Some disposable objects, however, may need to differentiate between disposal
+in a success context and disposal in an exception context as in the following
+example:
+
+```js
+class MyDisposableResource {
+  constructor() {
+    this.closed = false;
+  }
+
+  doSomething() {
+    if (maybeShouldThrow()) {
+      throw new Error('Something went wrong');
+    }
+  }
+
+  close() {
+    // Gracefully close the resource.
+    if (this.closed) return;
+    this.closed = true;
+    console.log('Resource closed');
+  }
+
+  abort(maybeError) {
+    // Abort the resource, optionally with an exception. Calling this
+    // method multiple times should not cause any issues or additional
+    // side effects.
+    if (this.closed) return;
+    this.closed = true;
+    if (maybeError) {
+      console.error('Resource aborted due to error:', maybeError);
+    } else {
+      console.log('Resource aborted');
+    }
+  }
+
+  [Symbol.dispose]() {
+    // Note that when this is called, we cannot pass any pending
+    // exceptions to the abort method because we do not know if
+    // there is a pending exception or not.
+    this.abort();
+  }
+}
+```
+
+Then in use:
+
+```js
+{
+  using resource = new MyDisposableResource();
+  // Do something with the resource that might throw an error
+  resource.doSomething();
+  // Explicitly close the resource if no error was thrown to
+  // avoid the resource being aborted when the `Symbol.dispose`
+  // method is called.
+  resource.close();
+}
+```
+
+Here, if an error is thrown in the `doSomething()` method, the `Symbol.dispose`
+method will still be called when the block exits, ensuring that the resource is
+disposed of properly using the `abort()` method. If no error is thrown, the
+`close()` method is called explicitly to gracefully close the resource. When the
+block exits, the `Symbol.dispose` method is still called but it will be a non-op
+since the resource has already been closed.
+
+To deal with errors that may occur during disposal, it is necessary to wrap
+the disposal block in a try-catch:
+
+```js
+try {
+  using resource = new MyDisposableResource();
+  // Do something with the resource that might throw an error
+  resource.doSomething();
+  resource.close();
+} catch (error) {
+  // Error might be the actual error thrown in the block, or might
+  // be a SuppressedError if an error was thrown during disposal and
+  // there was a pending exception already.
+  if (error instanceof SuppressedError) {
+    console.error('An error occurred during disposal masking pending error:',
+                  error.error, error.suppressed);
+  } else {
+    console.error('An error occurred:', error);
+  }
+}
+```
+
+### Symbol.dispose and Symbol.asyncDispose return values
+
+The `Symbol.dispose` method should return `undefined` and the
+`Symbol.asyncDispose` method should return a `Promise` that resolves to
+`undefined`.
+
+<!-- eslint-skip -->
+
+```js
+[Symbol.dispose]() {
+  return void this.dispose();
+  // or
+  this.dispose();
+  // or
+  return;
+  // or
+  // no return
+}
+
+async [Symbol.asyncDispose]() {
+  await this.dispose();
+  // or
+  return;
+  // or
+  // no return
+}
+```
+
+### Debuggability of disposer methods
+
+To improve debugging experience, The `Symbol.dispose` and `Symbol.asyncDispose`
+functions should not be direct aliases of named disposer functions. They should
+instead defer to the named disposer. This ensures the stack traces can be more
+informative about whether a disposer was called via `using` or was called
+directly.
+
+For example:
+
+<!-- eslint-skip -->
+
+```js
+// Do something like this:
+function dispose() { ... }
+return {
+  dispose,
+  [Symbol.dispose]() { this.dispose(); }
+};
+
+// Rather than this:
+function dispose() { ... }
+return {
+  dispose,
+  [Symbol.dispose]: dispose
+};
+```
+
+### A Note on documenting disposable objects
+
+When documenting disposable objects, it is important to clearly indicate that
+the object is disposable and how it should be disposed of. This includes
+documenting the `Symbol.dispose` and `Symbol.asyncDispose` methods, as well as
+any named disposal methods that the object exposes.
+
+If the disposable object is anonymous (that is, it is a regular JavaScript
+object that implements the `Symbol.dispose` method), it is still important to
+document that it is disposable and how it should be disposed of.
+
+Within the documentation, it is possible to document anonymous objects as if
+they were classes, using the `Class: ` prefix and otherwise presenting the
+object as if you were documenting a regular JavaScript class, even if it is
+never actually instantiated as a class. Examples of this pattern can be seen,
+for instance, in the documentation of the [Web Crypto API](../api/webcrypto.md).
+
+So, for example, if you have an API that returns an anonymous disposable object,
+you might document it like:
+
+```markdown
+### Class: `MyDisposableObject`
+
+#### `myDisposableObject.dispose()`
+
+...
+
+#### `myDisposableObject[Symbol.dispose]()`
+
+...
+
+### `foo.getMyDisposableObject()`
+
+* Returns: {MyDisposableObject}
+```
+
+## Guidelines for introducing explicit resource management into existing APIs
+
+Introducing the ability to use `using` into existing APIs can be tricky.
+
+The best way to understand the issues is to look at a real world example. PR
+[58516](https://github.com/nodejs/node/pull/58516) is a good case. This PR
+sought to introduce `Symbol.dispose` and `Symbol.asyncDispose` capabilities
+into the `fs.mkdtemp` API such that a temporary directory could be created and
+be automatically disposed of when the scope in which it was created exited.
+However, the existing implementation of the `fs.mkdtemp` API returns a string
+value that cannot be made disposable. There are also sync, callback, and
+promise-based variations of the existing API that further complicate the
+situation.
+
+In the initial proposal, the `fs.mkdtemp` API was changed to return an object
+that implements the `Symbol.dispose` method but only if a specific option is
+provided. This would mean that the return value of the API would become
+polymorphic, returning different types based on how it was called. This adds
+a lot of complexity to the API and makes it difficult to reason about the
+return value. It also makes it difficult to programmatically detect whether
+the version of the API being used supports `using` or not.
+`fs.mkdtemp('...', { disposable: true })` would act differently in older versions
+of Node.js than in newer versions with no way to detect this at runtime other
+than to inspect the return value.
+
+Some APIs that already return objects that can be made disposable do not have
+this kind of issue. For example, the `setImmediate()` API in Node.js returns an
+object that implements the `Symbol.dispose` method. This change was made without
+much fanfare because the return value of the API was already an object.
+
+So, some APIs can be made disposable easily without any issues while others
+require more thought and consideration. The following guidelines can help
+when introducing these capabilities into existing APIs:
+
+1. Avoid polymorphic return values: If an API already returns a value that
+   can be made disposable, and it makes sense to make it disposable, do so. Do
+   not, however, make the return value polymorphic determined by an option
+   passed into the API.
+2. Introduce new API variants that are `using` capable: If an existing API
+   cannot be made disposable without changing the return type or making it
+   polymorphic, consider introducing a new API variant. For example,
+   `fs.mkdtempDisposable` could be introduced to return a disposable object
+   while the existing `fs.mkdtemp` API continues to return a string. Yes, it
+   means more APIs to maintain but it avoids the complexity and confusion of
+   polymorphic return values. If adding a new API variant is not ideal, remember
+   that changing the return type of an existing API is quite likely a breaking
+   change.
+3. When an existing API signature does not lend itself easily to supporting making
+   the return value disposable and a new API needs to be introduced, it is worth
+   considering whether the existing API should be deprecated in favor of the new.
+   Deprecation is never a decision to be taken lightly, however, as it can have major
+   ecosystem impact.
+
+## Guidelines for using disposable objects
+
+Because disposable objects can be disposed of at any time, it is important
+to be careful when using them. Here are some guidelines for using disposables:
+
+1. Never use `using` or `await using` with disposable objects that you
+   do not own. For instance, the following code is problematic if you
+   are not the owner of `someObject`:
+
+```js
+function foo(someObject) {
+  using resource = someObject;
+}
+```
+
+The reason this is problematic is that the `using` statement will
+unconditionally call the `Symbol.dispose` method on `someObject` when the block
+exits, but you do not control the lifecycle of `someObject`. If `someObject`
+is disposed of, it may lead to unexpected behavior in the rest of the
+code that called the `foo` function.
+
+2. When there is a clear difference between disposing of an object in a success
+   context vs. an exception context, always explicitly dispose of objects the
+   successful code paths, including early returns. For example:
+
+```js
+class MyDisposableResource {
+  close() {
+    console.log('Resource closed');
+  }
+
+  abort() {
+    console.log('Resource aborted');
+  }
+
+  [Symbol.dispose]() {
+    // Assume the error case here...
+    this.abort();
+  }
+}
+
+function foo() {
+  using res = new MyDisposableResource();
+  if (someCondition) {
+    // Early return, ensure the resource is disposed of
+    res.close();
+    return;
+  }
+  // do other stuff
+  res.close();
+}
+```
+
+This is because of the fact that, when the disposer is called, it has no way
+of knowing if there is a pending exception or not and it is generally safest
+to assume that it is being called in an exceptional state.
+
+Many types of disposable objects make no differentiation between success and
+exception cases, in which case relying entirely on `using` is just fine (and
+preferred). The disposable returned by `setImmediate()` is a good example here.
+All that does is call `clearImmediate()` and it does not matter if the block
+errored or not.
+
+3. Remember that disposers are invoked in a stack, in the reverse order
+   in which they were created. For example,
+
+```js
+class MyDisposable {
+  constructor(name) {
+    this.name = name;
+  }
+  [Symbol.dispose]() {
+    console.log(`Disposing ${this.name}`);
+  }
+}
+
+{
+  using a = new MyDisposable('A');
+  using b = new MyDisposable('B');
+  using c = new MyDisposable('C');
+  // When this block exits, the disposal methods will be called in the
+  // reverse order: C, B, A.
+}
+```
+
+Because of this, it is important to consider the possible relationships
+between disposable objects. For example, if one disposable object holds a
+reference to another disposable object the cleanup order may be important.