using: A keyword that make be easily that resource variable management (JavaScript & TypeScript)

genie_oh

genie-oh

Posted on September 20, 2023

using: A keyword that make be easily that resource variable management (JavaScript & TypeScript)

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();
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode
class SomeResource implements AsyncDisposable {
  //...
  async [Symbol.asyncDispose]() {
    //... write to dispose resource with await
  }
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
genie_oh
genie-oh

Posted on September 20, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related