← Back to writing

Explicit Resource Management Has a Color Problem

The Explicit Resource Management proposal runs into a problem that feels familiar.

It’s the same class of problem that JavaScript had before TypeScript.

JavaScript gives you flexibility. TypeScript gives you guarantees.

TypeScript enhances JavaScript with types so you can reason about how data flows through your program. You no longer guess what a function expects. You no longer inspect implementations just to understand what kind of value comes back. The type system becomes an ambient source of truth.

Explicit Resource Management introduces a similar ambiguity, but for disposability.

How Do You Know Something Is Disposable?

Given a value, how do you know:

  • Should you use using?
  • Should you use await using?
  • Is it disposable at all?
  • Does it depend on context?

You cannot tell from the value itself.

You must:

  • Inspect its implementation
  • Check its documentation
  • Or try it and wait for TypeScript to complain

That last one is particularly revealing. TypeScript will tell you after the fact that a value does not implement Symbol.dispose or Symbol.asyncDispose. But that means you’re discovering disposability reactively rather than reasoning about it proactively.

This mirrors the classic async color problem.

With async functions, you cannot call an async function from a non-async context without changing the color of your code. The “color” propagates upward.

With Explicit Resource Management, we now introduce a new axis of color: disposability.

Some values are disposable. Some are not. Some require await using. Some require using. That color leaks upward into your call site.

Now you have to care.

The Upgrade Problem

There’s another subtle issue.

Imagine a library that returns a disposable resource. You correctly wrap it in using.

Later, the library updates. The resource is no longer disposable. Maybe the implementation changed.

Suddenly your program stops compiling.

Your logic didn’t change. Your intent didn’t change. But the color of the value changed.

A small change in the library becomes a sweeping refactor in your application, because the resource’s color leaked into your call graph.

“Just Use using Everywhere”

One might ask: why not just wrap everything in using and be done with it?

Because disposability is not universal.

It is conditional. It is opt-in. It is implementation-defined.

Which means the burden of correct usage sits with the caller.

That’s the same ergonomic tax that led people to embrace structured concurrency in other ecosystems.

A Different Model

The JavaScript proposal is a lightweight mechanism inspired by Rust’s ownership model.

But it is still caller-driven.

Rust does not require you to guess whether something needs cleanup. Ownership and lifetimes are part of the type system. The compiler enforces it. Cleanup is automatic and scoped by construction.

In JavaScript, we cannot retrofit borrow checking.

But we can move responsibility away from the caller.

Effection: Structured Resource Lifetimes

Effection tackles the same class of problems, but from a different direction.

Instead of asking the caller to manage disposal explicitly, it scopes resources to structured execution contexts.

When a function runs, any resource it creates is guaranteed to be cleaned up before its scope exits.

The caller does not manage disposal. The function owns its cleanup. The runtime enforces the boundary.

This eliminates the disposability color from the call site entirely.

Refactoring the MDN using Example

Here’s the MDN-style resource:

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 Explicit Resource Management, the caller would need to wrap usage in using.

With Effection, we can encapsulate lifecycle management:

import { resource } from "effection";

function acquireResource() {
  return resource(function* (provide) {
    let value;

    try {
      if (Math.random() < 0.5) {
        value = null;
      } else {
        value = new Resource();
      }

      yield* provide(value);
    } finally {
      value?.[Symbol.dispose]();
    }
  });
}

// ...
const resource = yield * acquireResource();
console.log(resource?.getValue());

Notice what changed.

The caller does not:

  • Inspect whether the value is disposable
  • Choose between using or await using
  • Manage cleanup

acquireResource handles setup and teardown internally.

Cleanup is structured. Guaranteed. Scoped.

The Real Question

Explicit Resource Management gives us a new tool.

But the deeper question is this:

Should resource lifetimes be something the caller has to remember?

Or should they be enforced by structure?

TypeScript solved JavaScript’s data-flow ambiguity by making types explicit.

Structured concurrency libraries like Effection attempt to solve lifecycle ambiguity by making scope the enforcement boundary.

One adds types. The other adds structure.

Both aim at the same thing: removing guesswork.