22 June, 202310 minute read

Cool language features: JavaScript’s using statement

Everyone knows what a memory leak is, and the prevalence of this bug is a testament to the difficulty of resource management in software engineering. Programs are useless without being able to interface with the surrounding system, and almost all such interfacing—such as reading or writing files, or connecting to a server over the network—involves acquiring and using resources of some kind. These resources then ultimately need to be released in order to avoid system failures from resource exhaustion.

There are a lot of different patterns for resource management, and they all have tradeoffs. I generally appreciate the “RAII” pattern—which you’ll find in C++, D, and Rust—for its elegance and composability, but at 30:34 in a video about ideas for a new games-focused programming language, Jonathan Blow discusses in great detail the mismatch between RAII and video game development. It’s important to pick the right tool for the job, and different languages offer varying syntax and user-space solutions for the problem of resource management.

Up until now, JavaScript has relied exclusively on user-space solutions and has lacked dedicated syntax for this problem. This is now changing, as a brand new RAII-ish using statement is being added to the spec. The using statement isn’t necessarily a replacement for existing patterns of resource management, but offers an additional option to engineers which complements today’s patterns nicely.

An overview of resource management patterns

The most basic and obvious form of resource management involves paired “open” and “close” functions. An example of this pattern in C are the fopen and fclose functions, which respectively open and close a file. There are many examples of this pattern available in the real world; the below code snippet shows how the Knex.js query builder leverages this pattern for its database transaction API. Throughout this article we’ll be looking at resource management through the lens of executing a database transaction, because this is a common real-world use case and the consequences for getting things wrong can be steep.

knex-transaction.js
Click to copy
const trx = await db.transaction(); try {  // Do some work with transaction  await trx.commit();} catch {  await trx.rollback();}

Something that makes this approach to resource management difficult is that the onus is on you to clean up the resource you’ve requested. In this stripped-down example it is trivial to see that our cleanup logic will always run, but in more complicated sections of business logic things can be less clear. When additional pieces of control flow or exception handling are mixed in, it can become challenging to:

  1. ensure that the cleanup logic is reachable by all code paths
  2. ensure that dependent objects are cleaned up in the correct order

The second point here tends to be more difficult to achieve than the first. Imagine that you’re trying to implement a copyFile function: you need to first open the source file so that you can read its contents, and then create a destination file you’ll write those contents to. Both of these operations involve file I/O, and both of them are therefore capable of failing.

This exact function is provided in the Go blog as a motivating example for the defer statement, which is the resource management pattern in use by languages such as Go and Zig. The defer statement essentially pushes a function call on to a stack which gets unwound when the enclosing scope terminates. This provides a guarantee that your cleanup functions will run no matter what so long as you remembered to defer them.

Here’s an example of how you might use a database transaction in Go:

go-transaction.go
Click to copy
func (s *service) CreateUser(ctx context.Context, dto UserDto) (*model.User, err error) {  tx, err := s.db.BeginTx(ctx, nil)  if err != nil {    return nil, err  }   // This logic runs when `CreateUser` returns!  defer func() {    if err != nil {      tx.Rollback()    }  }   // Create the user   err = tx.Commit()  if err != nil {    return nil, err  }   return user, nil}

While this does make it easier to ensure your cleanup code gets run, it’s still possible for developers to simply forget to call the cleanup code in the first place! This can be a costly mistake, and the previous code example is actually more or less directly taken from a blog post about this exact issue. A missing defer statement wound up costing the author’s company a few thousand dollars.

In languages with first-class functions like JavaScript, a more ergonomic API design is to instead take a callback. This allows the library to hide the resource management logic from consumers which keeps code lean and helps prevent silly errors creeping in to the codebase. Resource management is a deceptively difficult thing to do correctly. A real-world example of this pattern is below, showcasing the Prisma ORM’s interactive transactions API:

prisma-transaction.js
Click to copy
await db.$transaction(async (tx) => {  // Do some work with the transaction  // The `$transaction` method automatically commits/rolls back!});

The callback-based API removes all footguns related to running cleanup code (it’s no longer possible to accidentally keep a transaction open forever!) but it’s also not overly elegant. Using this style of API requires adding indentation to all of the code using the resource, which can make diffs pretty nasty to read and also creates another lexical scope. If you need to compute values inside of your transactional code, then it can be annoying to get those values out and into your code’s main scope.

All of these patterns can be used to great effect, but it’s clear that there are tradeoffs being made. In order to improve things, there’s a new and improved pattern coming to JavaScript in the near future which offers improved ergonomics and fewer footguns.

The new using statement

In the near future there will be a new using statement added to the JavaScript language which streamlines resource management compared to how it’s done today. There are two variants of this statement: a synchronous variant which was promoted to a Stage 3 proposal back in December ‘22, and an asynchronous variant which was promoted to Stage 3 in March of this year. .NET engineers will recognize this syntax, as the JavaScript proposal functions almost identically to the C# using declaration.

The using statement is essentially a new way of defining a variable binding, similar to let or const. The main difference is that when a binding defined by using goes out of lexical scope it is automatically “disposed.” To support this, there are two new well-known symbols called Symbol.dispose and Symbol.asyncDispose being added to the language spec which are intended to store the disposal function for a resource.

The following example shows a simple synchronous timer resource, which automatically logs out execution time when its scope ends:

using-sync.ts
Click to copy
function timer(name: string) {  const startedAt = performance.now();  return {    [Symbol.dispose]: () => {      const finishedAt = performance.now();      console.log(`${name} finished in ${finishedAt - startedAt}ms`);    },  };} function isEven(input: number) {  using t = timer('isEven');  return input % 2 === 0; // <- `t[Symbol.dispose]` is automatically called} isEven(4);// console: "isEven finished in 0.04ms"

The example is intentionally simple to focus on the syntax of the proposal, but the simplicity also highlights an important aspect of this new syntax. While the primary goal of the proposal is to streamline resource management, it’s also a useful pattern for creating small objects with helpful side effects.

In the previous example, our timer function doesn’t manage a resource at all—we’re simply leveraging the automatic invocation of Symbol.dispose as a convenient trigger for a log message. Tracking how long a function or other block of code takes to run doesn’t get any easier than this!

Of course, there is corresponding syntax for asynchronous resources. Here’s an example of how you might implement and use an async resource that’s compatible with the using statement:

using-async.ts
Click to copy
import fs from 'fs'; async function openFile(path: string) {  const handle = await fs.promises.open(path, 'r');   return {    handle,    [Symbol.asyncDispose]: async () => {      await handle.close();    },  }} {  await using file = openFile('data.json');   // ...} // File handle is automatically closed

Overall, the feature feels nice to use and definitely improves code readability. A lot of resource management bugs will likely be fixed by this syntax spreading across the ecosystem, which is a big win for our industry.

Something else which excites me is that the new syntax feels quite reminiscent of how resources are managed in C++. Over there resource management is tied to object lifetimes through class constructors and destructors and while JavaScript’s using statement isn’t quite the same thing, it’s a lot closer than constructs like Python’s with or Java’s try-with-resources.

It’s not quite RAII

C++-style RAII is based on object lifetime whereas the using statement is based on its containing lexical scope. While the lexical scope of an object and its lifetime are often equivalent, they are not the same thing and there are cases when the two can differ in duration. Return value optimization, for instance, allows an object created inside one function to be seamlessly moved into the caller’s stack frame and continue to live long after its lexical scope has ended.

The following C++ code exhibits this in action. If we were to write equivalent JavaScript code using using, we’d expect to see the “Destructed transaction” message to be logged before the “Received transaction”, as the Transaction instance gets disposed right before the get_transaction function returns. The resulting tx variable inside our calling function would then point at a garbage object with undefined behavior as it’s been cleaned up.

raii-lifetimes.cpp
Click to copy
#include <iostream> class Transaction {public:  Transaction() {    std::cout << "Constructed transaction\n";  }   ~Transaction() {    std::cout << "Destructed transaction\n";  }}; Transaction get_transaction() {  Transaction tx;  return tx;} int main() {  std::cout << "Entered main\n";  Transaction tx = get_transaction();  std::cout << "Received transaction\n";   return 0;}

In C++ the situation is different. Return value optimization kicks in which allows the wrapper we constructed inside get_transaction to be moved into the main function’s stack frame. If we run the code, we can see that the “Destructed transaction” message actually gets logged after the “Received transaction” message.

$ g++ main.cpp && ./a.out> Entered main> Constructed transaction> Received transaction> Destructed transaction

This is a pretty important distinction. In C++ we don’t really need to worry about how our Transaction gets cleaned up; we can simply call our factory function and use the object as-is. C++’s strong focus on value semantics and the proliferation of RAII in the standard library and wider ecosystem makes writing this style of code feel extremely natural.

JavaScript, on the other hand, requires us to think about these things. We can’t simply call our factory and move on with our day, because we can’t rely on a destructor to automatically clean up resources. Calling get_transaction requires us to throw in a using statement at the callsite, which means we need to concern ourselves with the cleanup logic of the underlying resource. Putting the using statement inside get_transaction would render the factory functionally useless.

There’s a vague similarity to async/await here, where the presence of a disposable JavaScript resource means a proliferation of using statements throughout your code. This issue can’t really be fixed given the design constraints of JavaScript: C++ achieves the level of composability that it does because destructors are called deterministically. JavaScript and other garbage collected languages can’t offer this guarantee, which is one of the reasons why true RAII is only found in a few languages.

How to upgrade old APIs

Upgrading old APIs so that they are compatible with the using statement is trivial, and as mentioned previously there are a number of interesting use cases which don’t involve resources at all.

Imagine that you’re using Jest or Vitest for testing. You might use a spy to verify that a function in your system gets called correctly, and generally you want to remove spies after each test finishes to help avoid test interdependency (^1 Mark Seeman has a great article involving hunting down a case of test interdependency here). There’s a useful resetAllMocks function you can call from inside a beforeEach or beforeAll lifecycle hook, but I’ve seen codebases where this isn’t possible to do because

Fortunately, the using statement can nicely solve our problem so long as we write a wrapper:

using-jest-spyon.ts
Click to copy
// Determining the correct types left// as an exercise to the readerfunction spyOn(...args: any[]) {  const spy = jest.spyOn(...args);  return {    spy,    [Symbol.dispose]: () => spy.mockRestore(),  };}

With only a few lines of code we’ve upgraded the spyOn API to be using-compatible. This allows us to set up a spy inside a test case and ensure it gets reliably reset without needing to touch any global hooks.

Most old APIs can be wrapped in much the same way. Leveraging this new language feature is very simple to do.

Be careful: there’s only one way to clean up

In the example of our database transaction, we have two paths which could be considered a form of “cleaning up.” The first is our happy path where our code executes successfully and we want to commit our transaction to save any mutations we may have made. The second case is where an error occurs and we need to roll back.

Unfortunately the dispose methods invoked by the using statement don’t receive any information about exceptions which may have been thrown. This means that the proposal is only appropriate for resources with one method of clean up. Files are an obvious example: you always want to close a file handle, regardless of whether an error has occurred.

This limitation has been pointed out in an issue, and I’m confident that there will be a follow on proposal to solve this limitation.

In the meantime, it’s possible to somewhat work around the issue. The workaround involves requiring consumers to manually call the happy path cleanup method, and running the error path cleanup method in your disposal function. Here’s an example of how you might wrap Knex’s transactions:

using-knex-transaction.ts
Click to copy
async function withTransaction(db: Knex) {  const tx = await db.transaction()(  let didCommit = false;   return {    tx,    commit: async () => {      await tx.commit();      didCommit = true;    },    [Symbol.asyncDispose]: async () => {      if (!didCommit) {        await tx.rollback();      }    },  };}

Be careful: lexical scope can sometimes be too long

Let’s revisit the problem of managing a database transaction. Today we use callback-based APIs to open and manage a database transaction, but in future we might start leveraging the using statement instead as it offers superior ergonomics. Transactional updates using Prisma may very well look like the following:

using-lexical-scope.ts
Click to copy
await using tx = db.$transaction(); // Do some stuff with the transaction // Do some other things return value;

There’s a potential problem here that’s hard to spot from the provided code, but becomes obvious when we shift back to callbacks:

callback-lexical-scope.ts
Click to copy
await db.$transaction(async (tx) => {  // Do some stuff with the transaction}); // Do some other things return value;

The new syntax makes it a lot easier to accidentally hold on to our transaction for longer than we really need it! We’ll release it eventually thanks to the guarantees offered by the using spec, but at large scales of operation holding on to a transaction too long can cause real latency problems which negatively impact user experience.

The fix is simple—we can easily wrap the code using tx inside a pair of braces to create a new lexical scope—but in my experience this can be hard for engineers to get right all of the time.

The indentation added in the callback-style API allows you to immediately see which code paths use the transaction and which ones don’t with only a quick glance, but the using-style API muddies the waters a little bit by keeping everything at the same indentation level.

When can you start to use using?

If you’re using Babel you can start using this new feature today via @babel/plugin-proposal-explicit-resource-management. TypeScript users on tsc will need to wait for TypeScript 5.2, which the team intends to ship on 22nd August this year.

For those of us using esbuild or swc for faster builds, there is no timeline available. swc has an open issue for this feature, but I couldn’t find anything for the esbuild project yet. Given that the proposal has reached Stage 3, I assume both of these transpilers will add support soon.

Conclusion

The new using statement is a big improvement to JavaScript’s resource management story, and enables extremely ergonomic RAII-style patterns. It’s available today in Babel, and will be available in the next couple of months for tsc users. Wrapping old libraries to support this new feature is simple to do, requiring only a few lines of code.

Engineers using this new syntax should be mindful that resources cleaned up via a using statement only support one method of clean up today, and that the using statement can easily result in holding on to resources for too long.

Don't want to miss out on new posts?

Join 100+ fellow engineers who subscribe for software insights, technical deep-dives, and valuable advice.

Get in touch 👋

If you're working on an innovative web or AI software product, then I'd love to hear about it. If we both see value in working together, we can move forward. And if not—we both had a nice chat and have a new connection.
Send me an email at hello@sophiabits.com