using
The using
declaration declares block-scoped local variables that are synchronously disposed. Like const
, variables declared with using
must be initialized and cannot be reassigned. The variable's value must be either null
, undefined
, or an object with a [Symbol.dispose]()
method. When the variable goes out of scope, the [Symbol.dispose]()
method of the object is called, to ensure that resources are freed.
Syntax
using name1 = value1;
using name1 = value1, name2 = value2;
using name1 = value1, name2 = value2, /* …, */ nameN = valueN;
nameN
-
The name of the variable to declare. Each must be a legal JavaScript identifier and not a destructuring binding pattern.
valueN
-
Initial value of the variable. It can be any legal expression but its value must be either
null
,undefined
, or an object with a[Symbol.dispose]()
method.
Description
This declaration can be used:
- Inside a block
- Inside any function body or class static initialization block
- At the top level of a module
- In the initializer of a
for
,for...of
, orfor await...of
loop
Most notably, it cannot be used:
- At the top level of a script, because script scopes are persistent.
- At the top level of a
switch
statement. - In the initializer of a
for...in
loop. Because the loop variable can only be a string or symbol, this doesn't make sense.
A using
declares a disposable resource that's tied to the lifetime of the variable's scope (block, function, module, etc.). When the scope exits, the resource is disposed of synchronously. The variable is allowed to have value null
or undefined
, so the resource can be optionally present.
When the variable is first declared and its value is non-nullish, a disposer is retrieved from the object. If the [Symbol.dispose]
property doesn't contain a function, a TypeError
is thrown. This disposer is saved to the scope.
When the variable goes out of scope, the disposer is called. If the scope contains multiple using
or await using
declarations, all disposers are run in the reverse order of declaration, regardless of the type of declaration. All disposers are guaranteed to run (much like the finally
block in try...catch...finally
). All errors thrown during disposal, including the initial error that caused the scope exit (if applicable), are all aggregated inside one SuppressedError
, with each earlier exception as the suppressed
property and the later exception as the error
property. This SuppressedError
is thrown after disposal is complete.
using
ties resource management to lexical scopes, which is both convenient and sometimes confusing. There are many ways to preserve the variable's value when the variable itself is out of scope, so you may hold a reference to an already-disposed resource. See below for some examples where it may not behave how you expect. If you want to hand-manage resource disposal, while maintaining the same error handling guarantees, you can use DisposableStack
instead.
Examples
In the following examples, we assume a simple Resource
class that has a getValue
method and a [Symbol.dispose]()
method:
class Resource {
value = Math.random();
#isDisposed = false;
getValue() {
if (this.#isDisposed) {
throw new Error("Resource is disposed");
}
return this.value;
}
[Symbol.dispose]() {
this.#isDisposed = true;
console.log("Resource disposed");
}
}
using
in a block
The resource declared with using
is disposed when exiting the block.
{
using resource = new Resource();
console.log(resource.getValue());
// resource disposed here
}
using
in a function
You can use using
in a function body. In this case, the resource is disposed when the function finishes executing, immediately before the function returns.
function example() {
using resource = new Resource();
return resource.getValue();
}
Here, resource[Symbol.dispose]()
will be called after getValue()
, before the return
statement executes.
The resource may outlive the declaration, in case it's captured by a closure:
function example() {
using resource = new Resource();
return () => resource.getValue();
}
In this case, if you call example()()
, you will always execute getValue
on a resource that's already disposed, because the resource was disposed when example
returns. In case you want to dispose the resource immediately after the callback has been called once, consider this pattern:
function example() {
const resource = new Resource();
return () => {
using resource2 = resource;
return resource2.getValue();
};
}
Here, we alias a const
-declared resource to a using
-declared resource, so that the resource is only disposed after the callback is called; note that if it is never called then the resource will never be cleaned up.
using
in a module
You can use using
at the top level of a module. In this case, the resource is disposed when the module finishes executing.
using resource = new Resource();
export const value = resource.getValue();
// resource disposed here
export using
is invalid syntax, but you can export
a variable declared elsewhere using using
:
using resource = new Resource();
export { resource };
This is still discouraged, because the importer will always receive a disposed resource. Similar to the closure problem, this causes the value of resource to live longer than the variable.
using
with for...of
You can use using
in the initializer of a for...of
loop. In this case, the resource is disposed on every loop iteration.
const resources = [new Resource(), new Resource(), new Resource()];
for (using resource of resources) {
console.log(resource.getValue());
// resource disposed here
}
Multiple using
The following are two equivalent ways to declare multiple disposable resources:
using resource1 = new Resource(),
resource2 = new Resource();
// OR
using resource1 = new Resource();
using resource2 = new Resource();
In both cases, when the scope exits, resource2
is disposed before resource1
. This is because resource2
may have a dependency on resource1
, so it's disposed first to ensure that resource1
is still available when resource2
is disposed.
Optional using
using
allows the variable to have value null
or undefined
, so the resource can be optionally present. This means you don't have to do this:
function acquireResource() {
// Imagine some real-world relevant condition here,
// such as whether there's space to allocate for this resource
if (Math.random() < 0.5) {
return null;
}
return new Resource();
}
const maybeResource = acquireResource();
if (maybeResource) {
using resource = maybeResource;
console.log(resource.getValue());
} else {
console.log(undefined);
}
But can do this:
using resource = acquireResource();
console.log(resource?.getValue());
using
declaration without using the variable
You can achieve automatic resource disposing using using
, without even using the variable. This is very useful for setting up a context within a block, such as creating a lock:
{
using _ = new Lock();
// Perform concurrent operations here
// Lock disposed (released) here
}
Note that _
is a normal identifier, but it's a convention to use it as a "throwaway" variable. To create multiple unused variables, you need to use distinct names, for example by using a variable name prefixed with _
.
Initialization and temporal dead zones
using
variables are subject to the same temporal dead zone restriction as let
and const
variables. This means that you can't access the variable before the initialization—the valid lifetime of the resource is strictly from its initialization to the end of its scope. This is enables RAII-style resource management.
let useResource;
{
useResource = () => resource.getValue();
useResource(); // Error: Cannot access 'resource' before initialization
using resource = new Resource();
useResource(); // Valid
}
useResource(); // Error: Resource is disposed
Error handling
The using
declaration is the most useful for managing resource disposal in the presence of errors. If you are not careful, some resources may leak because the error prevents code afterwards from executing.
function handleResource(resource) {
if (resource.getValue() > 0.5) {
throw new Error("Resource value too high");
}
}
try {
using resource = new Resource();
handleResource(resource);
} catch (e) {
console.error(e);
}
This will successfully catch the error thrown by handleResource
and log it, and no matter if handleResource
throws an error or not, the resource is disposed before exiting the try
block.
Here, if you don't use using
, you may do something like:
try {
const resource = new Resource();
handleResource(resource);
resource[Symbol.dispose]();
} catch (e) {
console.error(e);
}
But, if handleResource()
throws an error, then control never reaches resource[Symbol.dispose]()
, and the resource is leaked. Furthermore, if you have two resources, then errors thrown in earlier disposals may prevent later disposals from running, leading to more leaks.
Consider a more complicated case where the disposer itself throws an error:
class CantDisposeMe {
#name;
constructor(name) {
this.#name = name;
}
[Symbol.dispose]() {
throw new Error(`Can't dispose ${this.#name}`);
}
}
let error;
try {
using resource1 = new CantDisposeMe("resource1");
using resource2 = new CantDisposeMe("resource2");
throw new Error("Error in main block");
} catch (e) {
error = e;
}
You can inspect the error thrown in your browser's console. It has the following structure:
SuppressedError: An error was suppressed during disposal suppressed: SuppressedError: An error was suppressed during disposal suppressed: Error: Can't dispose resource1 error: Error: Error in main block error: Error: Can't dispose resource2
As you can see, error
contains all the errors that were thrown during disposal, as a SuppressedError
. Each additional error is added as the error
property, and the original error is added as the suppressed
property.
Specifications
No specification found
No specification data found for javascript.statements.using
.
Check for problems with this page or contribute a missing spec_url
to mdn/browser-compat-data. Also make sure the specification is included in w3c/browser-specs.