Resource management

For automatic cleanup of resources

Resources like database connections, file handles, and locks need cleanup. Traditional approaches are error-prone:

// 🚨 Manual cleanup is easy to forget
const conn = openConnection();
doWork(conn);
conn.close(); // What if doWork throws?
// 🚨 try/finally is verbose and doesn't compose
const conn = openConnection();
try {
  doWork(conn);
} finally {
  conn.close();
}

The using declaration is a new JavaScript feature that automatically disposes resources when they go out of scope:

const process = () => {
  using conn = openConnection();
  doWork(conn);
}; // conn is automatically disposed here

This works even if doWork throws—disposal is guaranteed.

Disposable resources

A resource is disposable if it has a [Symbol.dispose] method:

interface Disposable {
  [Symbol.dispose](): void;
}

For async cleanup, use [Symbol.asyncDispose] with await using:

interface AsyncDisposable {
  [Symbol.asyncDispose](): PromiseLike<void>;
}

Block scopes

Use block scopes to control exactly when resources are disposed:

const createLock = (name: string): Disposable => ({
  [Symbol.dispose]: () => {
    console.log(`unlock:${name}`);
  },
});

const process = () => {
  console.log("start");

  {
    using lock = createLock("a");
    console.log("critical-section-a");
  } // lock "a" released here

  console.log("between");

  {
    using lock = createLock("b");
    console.log("critical-section-b");
  } // lock "b" released here

  console.log("end");
};

// Output:
// "start"
// "critical-section-a"
// "unlock:a"
// "between"
// "critical-section-b"
// "unlock:b"
// "end"

Combining with Result

Result and Disposable are orthogonal:

  • Result answers: "Did the operation succeed?"
  • Disposable answers: "When do we clean up resources?"

Early returns from Result checks don't bypass using—disposal is guaranteed on any exit path (see below).

DisposableStack

When acquiring multiple resources, use DisposableStack to ensure all are cleaned up:

const processResources = (): Result<string, CreateResourceError> => {
  using stack = new DisposableStack();

  const db = createResource("db");
  if (!db.ok) return db; // stack disposes nothing yet

  stack.use(db.value);

  const file = createResource("file");
  if (!file.ok) return file; // stack disposes db

  stack.use(file.value);

  return ok("processed");
}; // stack disposes file, then db (reverse order)

The pattern is simple:

  1. Create a DisposableStack with using
  2. Try to create a resource (returns Result)
  3. If failed, return early—stack disposes what's been acquired
  4. If succeeded, add to stack with stack.use()
  5. Repeat for additional resources

For async resources, use AsyncDisposableStack with await using.

API overview:

  • stack.use(resource) — adds a disposable resource
  • stack.defer(fn) — adds a cleanup function (like Go's defer)
  • stack.adopt(value, cleanup) — wraps a non-disposable value with cleanup
  • stack.move() — transfers ownership to caller

The use-and-move pattern

When a factory function creates resources for use elsewhere, use move() to transfer ownership:

interface OpenFiles extends Disposable {
  readonly handles: ReadonlyArray<FileHandle>;
}

const openFiles = (
  paths: ReadonlyArray<string>,
): Result<OpenFiles, OpenFileError> => {
  using stack = new DisposableStack();

  const handles: Array<FileHandle> = [];
  for (const path of paths) {
    const file = open(path);
    if (!file.ok) return file; // Error: stack cleans up opened files

    stack.use(file.value);
    handles.push(file.value);
  }

  // Success: transfer ownership to caller
  const cleanup = stack.move();
  return ok({
    handles,
    [Symbol.dispose]: () => cleanup.dispose(),
  });
};

const processFiles = (): Result<void, MyError> => {
  const result = openFiles(["a.txt", "b.txt", "c.txt"]);
  if (!result.ok) return result;

  using files = result.value;

  // ... use files.handles ...

  return ok();
}; // files cleaned up here

Without move(), the stack would dispose files when openFiles returns—even on success.

Ready to use

TypeScript 5.2+ implements the using keyword, and Evolu polyfills runtime resource management for environments that still need it (Safari and React Native), see polyfills setup.

Learn more