The opinions expressed in this article are based on individual subjective views. Since the content of this article may contain errors, your understanding is appreciated. If you find any mistakes, differences in understanding, or have alternative opinions, comments are highly welcome!
Overview
In JavaScript, in addition to the keywords var
, let
, and const
, a new keyword using
is soon to be added for variable initialization.
Variables declared with the using
keyword will be treated as resource variables, allowing them to automatically perform resource cleanup before going out of scope. This is expected to simplify the cumbersome task of writing explicit resource cleanup code. The ECMAScript Proposal is currently at Stage 3, and you can track its status here. It will become a standard when it reaches Stage 4. For more details, please refer to The TC39 Process.
Additionally, TypeScript 5.2 introduces support for the using
keyword. Please note that you may currently need polyfill code for this feature.
This article will cover the following topics:
- Basic usage
- Addressing previous challenges and solutions
- How to use
using
in TypeScript 5.2 with code examples
Sample Code
You can see sample code on TypeScript 5.2 in the following GitHub repository: ts-explicit-resource-management.
Background
When handling with external resources such as file operations, database operations, and network communication (sockets), it is necessary to explicitly handle resource open and close (expose and dispose) operations.
To summarize, explicit resource handling like the one mentioned above has posed a considerable burden on programmers. To address this challenge, the using
keyword has been proposed and is expected to become a standard specification soon.
For example, when working with PostgreSQL using node-postgres
, you need to explicitly handle connect()
and close()
operations. Failing to call close()
can lead to issues where the DB connection's resources are not released, causing the process not to terminate.
Other programming languages like Java have features like AutoClosable
to address this issue, but JavaScript has lacked such an official feature until now. Therefore, programmers have had to come up with various workarounds, with the most common pattern being using try-finally
as shown below:
// ... inside an async function
let client: Client;
try {
client = new Client({...connectionInfo});
await client.connect();
// ... some logic
} catch (e) {
// ... error handling
} finally {
await client.close();
}
However, even with this approach, writing try-finally
for each usage point or trying to avoid it and creating a Wrapper Class for automatic cleanup presented various challenges. One significant challenge was maintaining code consistency among team members when it came to resource management, which I personally found to be quite difficult.
Basic Syntax
With the using
keyword, you can declare resource variables that have a Symbol.dispose
symbol key, which defines a callback function for resource cleanup. When a resource variable declared with using
goes out of scope, the function defined as Symbol.dispose
is automatically called, performing the resource cleanup.
Here's an example:
const getResource = () => {
// ... create initialization code for the resource you want to use
return {
// Variables used with 'using' have a symbol key 'Symbol.dispose'
// that holds a callback function for resource disposal
[Symbol.dispose]: () => {
// ... create resource cleanup logic
},
};
};
function doWorkOnResource() {
// Declare a resource variable with the 'using' keyword
using resource = getResource();
// ... some logic
return;
// When the 'using' variable goes out of scope, disposal is automatic
}
If you want to work with asynchronous operations, similar to async/await
, you need to use Symbol.asyncDispose
:
const getResource = () =>
//...
[Symbol.asyncDispose]: async () => {
// ... write to dispose resource with await
},
//...
async function doWorkOnResource() {
await using resource = await getResource();
}
Additionally, you can implement this with classes by implementing the Disposable
and AsyncDisposable
interfaces:
class SomeResource implements Dispoable {
//...
async [Symbol.dispose]() {
//... write to dispose resource
}
class SomeResource implements AsyncDisposable {
//...
async [Symbol.asyncDispose]() {
//... write to dispose resource with await
}
Use Cases
Here, we'll introduce an example of connecting to PostgreSQL using node-postgres
, performing a select operation, and then closing the connection.
You can find sample code in the following GitHub repository: ts-explicit-resource-management.
pg-try-finally.example.ts
This example demonstrates resource handling using the traditional try-catch-finally
approach:
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
/**
* @throws Error: errors from pg
*/
const isAvailableDBConnection = async () => {
let client: Client;
try {
// Try to connect
client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
await client.connect();
// Query
const res = await client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message); // Hello world!
// Return true if the query is successful
return true;
} catch (e) {
console.error(e);
throw e;
} finally {
console.debug("Try to client.end()");
await client.end();
}
};
isAvailableDBConnection();
pg-asyncDispose-func.example.ts
This example demonstrates the usage of using
and asyncDispose
. It is implemented as an AsyncDisposable Function:
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");
/**
* @returns: { client: pg.Client, [Symbol.asyncDispose]: dispose function }
*/
const getDBConnection = async () => {
// Try to connect
const client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
await client.connect();
// Return resource as disposable
return {
client,
[Symbol.asyncDispose]: async () => {
console.debug("Try to client.end()");
await client.end();
},
};
};
/**
* @throws Error: errors from pg
*/
const isAvailableDBConnection = async () => {
// Declare resource variable by the `using` keyword
await using db = await getDBConnection();
const res = await db.client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message);
// ...
// Before going out of scope, the resource will be disposed by the function of [Symbol.asyncDispose]
};
isAvailableDBConnection();
pg-asyncDispose-class.example.ts
This example demonstrates the usage of using
and asyncDispose
. It is implemented as an AsyncDisposable Class:
import * as dotenv from "dotenv";
dotenv.config();
import { Client } from "pg";
// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");
/**
* Disposable class
*/
class DBConnection implements AsyncDisposable {
private client: Client;
private constructor() {
this.client = new Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: +process.env.DB_PORT,
});
}
/**
* Factory method
* @returns: new instance of DBConnection
*/
static async of() {
const instance = new DBConnection();
await instance.connect();
return instance;
}
/**
* Implemented by the asyncDisposable interface
*/
async [Symbol.asyncDispose]() {
console.debug("Try to client.end()");
await this.client.end();
}
getClient() {
return this.client;
}
private async connect() {
await this.client.connect();
}
}
/**
* @throws Error: errors from pg
*/
const isAvailableDBConnection = async () => {
// Declare resource variable by the `using` keyword
await using db = await DBConnection.of();
const client = db.getClient()
const res = await client.query("SELECT $1::text as message", [
"Hello world!",
]);
console.info(res.rows[0].message);
// ...
// Before going out of scope, the resource will be disposed by the function of [Symbol.asyncDispose]
};
isAvailableDBConnection();
Conclusion (my opinion)
I think the most significant advantage of the using
keyword is the standardization it brings. While the using
keyword may not solve all the issues mentioned above, it is expected to bring significant improvements.
In my experience, maintaining code for "resource management" in team development has often been challenging due to differences in how team members approach it and potential oversights that lead to complex runtime problems.
Recently, I have been addressing this by adopting frameworks that allow for Dependency Injection (DI), managing external resources within Singleton or method-level lifecycles, and then standardizing the approach as a team. However, this approach also comes with its challenges, especially during initial development and achieving common understanding.
Additionally, the management of external resource variables, which goes beyond just databases, has been an ongoing challenge.
Therefore, the introduction of the using
keyword as a standard feature is expected to naturally lead JavaScript and TypeScript programmers to acquire this knowledge. This, in turn, can simplify resource management in team development and help maintain code quality.
References
- ECMAScript:proposal-explicit-resource-management
- Announcing TypeScript 5.2: using Declarations and Explicit Resource Management
- Resource management in TypeScript with the using keyword
Top comments (0)