|  | 
|  | 1 | +# Explicit Resource Management (`using`) Guidelines | 
|  | 2 | + | 
|  | 3 | +Explicit Resource Management is a capability that was introduced to the JavaScript | 
|  | 4 | +language in 2025. It provides a way of marking objects as disposable resources such | 
|  | 5 | +that the JavaScript engine will automatically invoke disposal methods when the | 
|  | 6 | +object is no longer in scope. For example: | 
|  | 7 | + | 
|  | 8 | +```js | 
|  | 9 | +class MyResource { | 
|  | 10 | +  dispose() { | 
|  | 11 | +    console.log('Resource disposed'); | 
|  | 12 | +  } | 
|  | 13 | + | 
|  | 14 | +  [Symbol.dispose]() { | 
|  | 15 | +    this.dispose(); | 
|  | 16 | +  } | 
|  | 17 | +} | 
|  | 18 | + | 
|  | 19 | +{ | 
|  | 20 | +  using resource = new MyResource(); | 
|  | 21 | +  // When this block exits, the `Symbol.dispose` method will be called | 
|  | 22 | +  // automatically by the JavaScript engine. | 
|  | 23 | +} | 
|  | 24 | +``` | 
|  | 25 | + | 
|  | 26 | +This document outlines some specific guidelines for using explicit resource | 
|  | 27 | +management in the Node.js project -- specifically, guidelines around how to | 
|  | 28 | +make objects disposable and how to introduce the new capabilities into existing | 
|  | 29 | +APIs. | 
|  | 30 | + | 
|  | 31 | +There is a significant caveat to this document, however. Explicit resource | 
|  | 32 | +management is brand new and there is not a body of experience to draw from | 
|  | 33 | +when writing these guidelines. The points outlined here are based on the | 
|  | 34 | +current understanding of how the mechanism works and how it is expected to | 
|  | 35 | +be used. As such, these guidelines may change over time as more experience | 
|  | 36 | +is gained with explicit resource management in Node.js and the ecosystem. | 
|  | 37 | +It is always a good idea to check the latest version of this document, and | 
|  | 38 | +more importantly, to suggest changes to it based on evolving understanding, | 
|  | 39 | +needs, and experience. | 
|  | 40 | + | 
|  | 41 | +## Some background | 
|  | 42 | + | 
|  | 43 | +Objects can be made disposable by implementing either, or both, the | 
|  | 44 | +`Symbol.dispose` and `Symbol.asyncDispose` methods: | 
|  | 45 | + | 
|  | 46 | +```js | 
|  | 47 | +class MySyncResource { | 
|  | 48 | +  [Symbol.dispose]() { | 
|  | 49 | +    // Synchronous disposal logic | 
|  | 50 | +  } | 
|  | 51 | +} | 
|  | 52 | + | 
|  | 53 | +class MyAsyncDisposableResource { | 
|  | 54 | +  async [Symbol.asyncDispose]() { | 
|  | 55 | +    // Asynchronous disposal logic | 
|  | 56 | +  } | 
|  | 57 | +} | 
|  | 58 | +``` | 
|  | 59 | + | 
|  | 60 | +An object that implements `Symbol.dispose` can be used with the `using` | 
|  | 61 | +statement, which will automatically call the `Symbol.dispose` method when the | 
|  | 62 | +object goes out of scope. If an object implements `Symbol.asyncDispose`, it can | 
|  | 63 | +be used with the `await using` statement in an asynchronous context. It is | 
|  | 64 | +worth noting here that `await using` means the disposal is asynchronous, | 
|  | 65 | +not the initialization. | 
|  | 66 | + | 
|  | 67 | +```mjs | 
|  | 68 | +{ | 
|  | 69 | +  using resource = new MyResource(); | 
|  | 70 | +  await using asyncResource = new MyResource(); | 
|  | 71 | +} | 
|  | 72 | +``` | 
|  | 73 | + | 
|  | 74 | +Importantly, it is necessary to understand that the design of `using` makes it | 
|  | 75 | +possible for user code to call the `Symbol.dispose` or `Symbol.asyncDispose` | 
|  | 76 | +methods directly, outside of the `using` or `await using` statements. These | 
|  | 77 | +can also be called multiple times and by any code that is holding a reference | 
|  | 78 | +to the object. That is to say, explicit resource management does not imply | 
|  | 79 | +ownership of the object. It is not a form of RAII (Resource Acquisition Is | 
|  | 80 | +Initialization) as seen in some other languages and there is no notion of | 
|  | 81 | +exclusive ownership of the object. A disposable object can become disposed | 
|  | 82 | +at any time. | 
|  | 83 | + | 
|  | 84 | +The `Symbol.dispose` and `Symbol.asyncDispose` methods are called in both | 
|  | 85 | +successful and exceptional exits from the scopes in which the using keyword | 
|  | 86 | +is used. This means that if an exception is thrown within the scope, the | 
|  | 87 | +disposal methods will still be called. However, when the disposal methods are | 
|  | 88 | +called they are not aware of the context. These methods will not receive any | 
|  | 89 | +information about the exception that was thrown or whether an exception was | 
|  | 90 | +thrown at all. This means that it is often safest to assume that the disposal | 
|  | 91 | +methods will be called in a context where the object may not be in a valid | 
|  | 92 | +state or that an exception may be pending. | 
|  | 93 | + | 
|  | 94 | +## Guidelines for Disposable Objects | 
|  | 95 | + | 
|  | 96 | +So with this is mind, it is necessary to outline some guidelines for disposers: | 
|  | 97 | + | 
|  | 98 | +1. Disposers should be idempotent. Multiple calls to the disposal methods | 
|  | 99 | +   should not cause any issues or have any additional side effects. | 
|  | 100 | +2. Disposers should assume that it is being called in an exception context. | 
|  | 101 | +   Always assume there is likely a pending exception and that if the object | 
|  | 102 | +   has not been explicitly closed when the disposal method is called, the | 
|  | 103 | +   object should be disposed as if an exception had occurred. For instance, | 
|  | 104 | +   if the object API exposes both a `close()` method and an `abort()` method, | 
|  | 105 | +   the disposal method should call `abort()` if the object is not already | 
|  | 106 | +   closed. If there is no difference in disposing in success or exception | 
|  | 107 | +   contexts, then separate disposal methods are unnecessary. | 
|  | 108 | +3. It is recommended to avoid throwing errors within disposers. | 
|  | 109 | +   If a disposer throws an exception while there is another pending | 
|  | 110 | +   exception, then both exceptions will be wrapped in a `SupressedError` | 
|  | 111 | +   that masks both. This makes it difficult to understand the context | 
|  | 112 | +   in which the exceptions were thrown. | 
|  | 113 | +4. Disposable objects should expose explicit disposal methods in addition | 
|  | 114 | +   to the `Symbol.dispose` and `Symbol.asyncDispose` methods. This allows | 
|  | 115 | +   user code to explicitly dispose of the object without using the `using` | 
|  | 116 | +   or `await using` statements. For example, a disposable object might | 
|  | 117 | +   expose a `close()` method that can be called to dispose of the object. | 
|  | 118 | +   The `Symbol.dispose` and `Symbol.asyncDispose` methods should delegate to | 
|  | 119 | +   these explicit disposal methods. | 
|  | 120 | +5. Because it is safest to assume that the disposal method will be called | 
|  | 121 | +   in an exception context, it is generally recommended to prefer use of | 
|  | 122 | +   `Symbol.dispose` over `Symbol.asyncDispose` when possible. Asynchronous | 
|  | 123 | +   disposal can lead delaying the handling of exceptions and can make it | 
|  | 124 | +   difficult to reason about the state of the object while the disposal is | 
|  | 125 | +   in progress. Disposal in an exception context is preferably synchronous | 
|  | 126 | +   and immediate. That said, for some types of objects async disposal is not | 
|  | 127 | +   avoidable. | 
|  | 128 | +6. Asynchronous disposers, by definition, are able to yield to other tasks | 
|  | 129 | +   while waiting for their disposal task(s) to complete. This means that, as a | 
|  | 130 | +   minimum, a `Symbol.asyncDispose` method must be an `async` function, and | 
|  | 131 | +   must `await` at least one asynchronous disposal task. If either of these | 
|  | 132 | +   criteria is not met, then the disposer is actually a synchronous disposer in | 
|  | 133 | +   disguise, and will block the execution thread until it returns; such a | 
|  | 134 | +   disposer should instead be declared using `Symbol.dispose`. | 
|  | 135 | +7. Avoid, as much as possible, using both `Symbol.dispose` and `Symbl.asyncDispose` | 
|  | 136 | +   in the same object. This can make it difficult to reason about which method | 
|  | 137 | +   will be called in a given context and could lead to unexpected behavior or | 
|  | 138 | +   subtle bugs. This is not a firm rule, however. Sometimes it may make sense | 
|  | 139 | +   to define both but likely not. | 
|  | 140 | + | 
|  | 141 | +### Example Disposable Object | 
|  | 142 | + | 
|  | 143 | +```js | 
|  | 144 | +class MyDisposableResource { | 
|  | 145 | +  constructor() { | 
|  | 146 | +    this.closed = false; | 
|  | 147 | +  } | 
|  | 148 | + | 
|  | 149 | +  doSomething() { | 
|  | 150 | +    if (maybeShouldThrow()) { | 
|  | 151 | +      throw new Error('Something went wrong'); | 
|  | 152 | +    } | 
|  | 153 | +  } | 
|  | 154 | + | 
|  | 155 | +  close() { | 
|  | 156 | +    // Gracefully close the resource. | 
|  | 157 | +    if (this.closed) return; | 
|  | 158 | +    this.closed = true; | 
|  | 159 | +    console.log('Resource closed'); | 
|  | 160 | +  } | 
|  | 161 | + | 
|  | 162 | +  abort(maybeError) { | 
|  | 163 | +    // Abort the resource, optionally with an exception. Calling this | 
|  | 164 | +    // method multiple times should not cause any issues or additional | 
|  | 165 | +    // side effects. | 
|  | 166 | +    if (this.closed) return; | 
|  | 167 | +    this.closed = true; | 
|  | 168 | +    if (maybeError) { | 
|  | 169 | +      console.error('Resource aborted due to error:', maybeError); | 
|  | 170 | +    } else { | 
|  | 171 | +      console.log('Resource aborted'); | 
|  | 172 | +    } | 
|  | 173 | +  } | 
|  | 174 | + | 
|  | 175 | +  [Symbol.dispose]() { | 
|  | 176 | +    // Note that when this is called, we cannot pass any pending | 
|  | 177 | +    // exceptions to the abort method because we do not know if | 
|  | 178 | +    // there is a pending exception or not. | 
|  | 179 | +    this.abort(); | 
|  | 180 | +  } | 
|  | 181 | +} | 
|  | 182 | +``` | 
|  | 183 | + | 
|  | 184 | +Then in use: | 
|  | 185 | + | 
|  | 186 | +```js | 
|  | 187 | +{ | 
|  | 188 | +  using resource = new MyDisposableResource(); | 
|  | 189 | +  // Do something with the resource that might throw an error | 
|  | 190 | +  resource.doSomething(); | 
|  | 191 | +  resource.close(); | 
|  | 192 | +} | 
|  | 193 | +``` | 
|  | 194 | + | 
|  | 195 | +Here, if an error is thrown in the `doSomething()` method, the `Symbol.dispose` | 
|  | 196 | +method will still be called when the block exits, ensuring that the resource is | 
|  | 197 | +disposed of properly using the `abort()` method. If no error is thrown, the | 
|  | 198 | +`close()` method is called explicitly to gracefully close the resource. When the | 
|  | 199 | +block exits, the `Symbol.dispose` method is still called but it will be a non-op | 
|  | 200 | +since the resource has already been closed. | 
|  | 201 | + | 
|  | 202 | +To deal with errors that may occur during disposal, it is necessary to wrap | 
|  | 203 | +the disposal block in a try-catch: | 
|  | 204 | + | 
|  | 205 | +```js | 
|  | 206 | +try { | 
|  | 207 | +  using resource = new MyDisposableResource(); | 
|  | 208 | +  // Do something with the resource that might throw an error | 
|  | 209 | +  resource.doSomething(); | 
|  | 210 | +  resource.close(); | 
|  | 211 | +} catch (error) { | 
|  | 212 | +  // Error might be the actual error thrown in the block, or might | 
|  | 213 | +  // be a SuppressedError if an error was thrown during disposal and | 
|  | 214 | +  // there was a pending exception already. | 
|  | 215 | +  if (error instanceof SuppressedError) { | 
|  | 216 | +    console.error('An error occurred during disposal masking pending error:', | 
|  | 217 | +                  error.error, error.suppressed); | 
|  | 218 | +  } else { | 
|  | 219 | +    console.error('An error occurred:', error); | 
|  | 220 | +  } | 
|  | 221 | +} | 
|  | 222 | +``` | 
|  | 223 | + | 
|  | 224 | +## Guidelines for Introducing explicit resource management into Existing APIs | 
|  | 225 | + | 
|  | 226 | +Introducing the ability to use `using` into existing APIs can be tricky. | 
|  | 227 | + | 
|  | 228 | +The best way to understand the issues is to look at a real world example. PR | 
|  | 229 | +[58516](https://github.com/nodejs/node/pull/58516) is a good case. This PR | 
|  | 230 | +sought to introduce `Symbol.dispose` and `Symbol.asyncDispose` capabilities | 
|  | 231 | +into the `fs.mkdtemp` API such that a temporary directory could be created and | 
|  | 232 | +be automatically disposed of when the scope in which it was created exited. | 
|  | 233 | +However, the existing implementation of the `fs.mkdtemp` API returns a string | 
|  | 234 | +value that cannot be made disposable. There are also sync, callback, and | 
|  | 235 | +promise-based variations of the existing API that further complicate the | 
|  | 236 | +situation. | 
|  | 237 | + | 
|  | 238 | +In the initial proposal, the `fs.mkdtemp` API was changed to return an object | 
|  | 239 | +that implements the `Symbol.dispose` method but only if a specific option is | 
|  | 240 | +provided. This would mean that the return value of the API would become | 
|  | 241 | +polymorphic, returning different types based on how it was called. This adds | 
|  | 242 | +a lot of complexity to the API and makes it difficult to reason about the | 
|  | 243 | +return value. It also makes it difficult to programmatically detect whether | 
|  | 244 | +the version of the API being used supports `using` or not. | 
|  | 245 | +`fs.mkdtemp('...', { disposable: true })` would act differently in older versions | 
|  | 246 | +of Node.js than in newer versions with no way to detect this at runtime other | 
|  | 247 | +than to inspect the return value. | 
|  | 248 | + | 
|  | 249 | +Some APIs that already return objects that can be made disposable do not have | 
|  | 250 | +this kind of issue. For example, the `setTimeout()` API in Node.js returns an | 
|  | 251 | +object that implements the `Symbol.dispose` method. This change was made without | 
|  | 252 | +much fanfare because the return value of the API was already an object. | 
|  | 253 | + | 
|  | 254 | +So, some APIs can be made disposable easily without any issues while others | 
|  | 255 | +require more thought and consideration. The following guidelines can help | 
|  | 256 | +when introducing these capabilities into existing APIs: | 
|  | 257 | + | 
|  | 258 | +1. Avoid polymorphic return values: If an API already returns a value that | 
|  | 259 | +   can be made disposable, and it makes sense to make it disposable, do so. Do | 
|  | 260 | +   not, however, make the return value polymorphic determined by an option | 
|  | 261 | +   passed into the API. | 
|  | 262 | +2. Introduce new API variants that are `using` capable: If an existing API | 
|  | 263 | +   cannot be made disposable without changing the return type or making it | 
|  | 264 | +   polymorphic, consider introducing a new API variant. For example, | 
|  | 265 | +   `fs.mkdtempDisposable` could be introduced to return a disposable object | 
|  | 266 | +   while the existing `fs.mkdtemp` API continues to return a string. Yes, it | 
|  | 267 | +   means more APIs to maintain but it avoids the complexity and confusion of | 
|  | 268 | +   polymorphic return values. If adding a new API variant is not ideal, remember | 
|  | 269 | +   that changing the return type of an existing API is quite likely a breaking | 
|  | 270 | +   change. | 
|  | 271 | +3. When an existing API signature does not lend itself easily to supporting making | 
|  | 272 | +   the return value disposable and a new API needs to be introduced, it is worth | 
|  | 273 | +   considering whether the existing API should be deprecated in favor of the new. | 
|  | 274 | +   Deprecation is never a decision to be taken lightly, however, as it can have major | 
|  | 275 | +   ecosystem impact. | 
|  | 276 | + | 
|  | 277 | +## Guidelines for using disposable objects | 
|  | 278 | + | 
|  | 279 | +Because disposable objects can be disposed of at any time, it is important | 
|  | 280 | +to be careful when using them. Here are some guidelines for using disposable: | 
|  | 281 | + | 
|  | 282 | +1. Never use `using` or `await using` with disposable objects that you | 
|  | 283 | +   do not own. For instance, the following code is problematic if you | 
|  | 284 | +   are not the owner of `someObject`: | 
|  | 285 | + | 
|  | 286 | +```js | 
|  | 287 | +function foo(someObject) { | 
|  | 288 | +  using resource = someObject; | 
|  | 289 | +} | 
|  | 290 | +``` | 
|  | 291 | + | 
|  | 292 | +The reason this is problematic is that the `using` statement will | 
|  | 293 | +unconditionally call the `Symbol.dispose` method on `someObject` when the block | 
|  | 294 | +exits, but you do not control the lifecycle of `someObject`. If `someObject` | 
|  | 295 | +is disposed of, it may lead to unexpected behavior in the rest of the | 
|  | 296 | +code that called the `foo` function. | 
|  | 297 | + | 
|  | 298 | +2. When there is a clear difference between disposing of an object in a success | 
|  | 299 | +   context vs. an exception context, always explicitly dispose of objects the | 
|  | 300 | +   successful code paths, including early returns. For example: | 
|  | 301 | + | 
|  | 302 | +```js | 
|  | 303 | +class MyDisposableResource { | 
|  | 304 | +  close() { | 
|  | 305 | +    console.log('Resource closed'); | 
|  | 306 | +  } | 
|  | 307 | + | 
|  | 308 | +  abort() { | 
|  | 309 | +    console.log('Resource aborted'); | 
|  | 310 | +  } | 
|  | 311 | + | 
|  | 312 | +  [Symbol.dispose]() { | 
|  | 313 | +    // Assume the error case here... | 
|  | 314 | +    this.abort(); | 
|  | 315 | +  } | 
|  | 316 | +} | 
|  | 317 | + | 
|  | 318 | +function foo() { | 
|  | 319 | +  using res = new MyDisposableResource(); | 
|  | 320 | +  if (someCondition) { | 
|  | 321 | +    // Early return, ensure the resource is disposed of | 
|  | 322 | +    res.close(); | 
|  | 323 | +    return; | 
|  | 324 | +  } | 
|  | 325 | +  // do other stuff | 
|  | 326 | +  res.close(); | 
|  | 327 | +} | 
|  | 328 | +``` | 
|  | 329 | + | 
|  | 330 | +This is because of the fact that, when the disposer is called, it has no way | 
|  | 331 | +of knowing if there is a pending exception or not and it is generally safest | 
|  | 332 | +to assume that it is being called in an exceptional state. | 
|  | 333 | + | 
|  | 334 | +Many types of disposable objects make no differentiation between success and | 
|  | 335 | +exception cases, in which case relying entirely on `using` is just fine (and | 
|  | 336 | +preferred). The disposable returned by `setTimeout()` is a good example here. | 
|  | 337 | +All that does is call `clearTimeout()` and it does not matter if the block | 
|  | 338 | +errored or not. | 
|  | 339 | + | 
|  | 340 | +3. Remember that disposers are invoked in a stack, in the reverse order | 
|  | 341 | +   in which there were created. For example, | 
|  | 342 | + | 
|  | 343 | +```js | 
|  | 344 | +class MyDisposable { | 
|  | 345 | +  constructor(name) { | 
|  | 346 | +    this.name = name; | 
|  | 347 | +  } | 
|  | 348 | +  [Symbol.dispose]() { | 
|  | 349 | +    console.log(`Disposing ${this.name}`); | 
|  | 350 | +  } | 
|  | 351 | +} | 
|  | 352 | + | 
|  | 353 | +{ | 
|  | 354 | +  using a = new MyDisposable('A'); | 
|  | 355 | +  using b = new MyDisposable('B'); | 
|  | 356 | +  using c = new MyDisposable('C'); | 
|  | 357 | +  // When this block exits, the disposal methods will be called in the | 
|  | 358 | +  // reverse order: C, B, A. | 
|  | 359 | +} | 
|  | 360 | +``` | 
|  | 361 | + | 
|  | 362 | +Because of this, it is important to consider the possible relationships | 
|  | 363 | +between disposable objects. For example, if one disposable object holds a | 
|  | 364 | +reference to another disposable object the cleanup order may be important. | 
|  | 365 | +If disposers are properly idempotent, however, this should not cause any | 
|  | 366 | +issue, but it still requires careful consideration. | 
0 commit comments