npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sirpepe/no-use-after-free

v0.1.1

Published

Prevent use-after-free when working with the explicit resource management API

Downloads

222

Readme

no-use-after-free.js

👉 npm i @sirpepe/no-use-after-free

JavaScript's Explicit Resource Management API does not by itself prevent use-after-free:

// Class representing some sort of resource
class Example {
  doSomething() {
    console.log("Doing something");
  }
  // Logic to clean up the resource
  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

function test() {
  using resource = new Example(); // "using" ensures Symbol.dispose runs at the end of the function
  resource.doSomething(); // logs "Doing something"
  return resource; // returns/leaks the resource that gets cleaned up at the end of the function
  // Symbol.dispose runs here, logs "Cleaning up"
}

let instance = test();
instance.doSomething(); // STILL logs "Doing something" ⚠️

no-use-after-free.js prevents use-after-free with Explicit Resource Management at run-time. Every interaction with an object that has had its Symbol.dispose or Symbol.asyncDispose run to completion results in exceptions.

class Example {
  doSomething() {
    console.log("Doing something");
  }
  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

import noUseAfterFree from "@sirpepe/no-use-after-free";

function test() {
  using resource = noUseAfterFree(new Example()); // add cleanup tracking
  resource.doSomething(); // works
  return resource;
  // Symbol.dispose runs here, logs "Cleaning up"
}

let instance = test();
instance.doSomething(); // Throws an exception ✅

To further automate this, you can add the cleanup tracking directly in your object's factory functions or class constructors:

import noUseAfterFree from "@sirpepe/no-use-after-free";

class Example {
  constructor() {
    return noUseAfterFree(this); // yes this works. Yes, even with TypeScript
  }
  doSomething() {
    console.log("Doing something");
  }
  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

The main function also works as a class decorator:

import noUseAfterFree from "@sirpepe/no-use-after-free";

@noUseAfterFree
class Example {
  doSomething() {
    console.log("Doing something");
  }
  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

function test() {
  using resource = new Example();
  resource.doSomething(); // works
  return resource;
  // Symbol.dispose runs here, logs "Cleaning up"
}

let instance = test();
resource.doSomething(); // fails

To get the decorator syntax working in early 2026, you will probably need some tooling support, such as:

How does this work?

This library works by wrapping disposable objects in revocable proxies that render the objects unusable once either Symbol.dispose or Symbol.asyncDispose have run to completion. Despite the wrapper, private class fields and private methods should keep working out of the box - the proxy redirects this values appropriately

Non-cursed edge cases

[!IMPORTANT] Everything in the section this only applies when you don't use the class decorator syntax!

An edge case that does not work without manual intervention is static methods accessing private fields on instances of non-decorated classes:

import noUseAfterFree from "@sirpepe/no-use-after-free";

class Example {
  #x = 1;

  static readX(instance) {
    return instance.#x;
  }

  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

using instance1 = new Example();
Example.readX(instance1); // OK

using instance2 = noUseAfterFree(new Example());
Example.readX(instance3); // Error

This fails because the object that gets passed to the static method is an instance wrapped in the tracking proxy - and the proxy, not being an actual instance of the class, is not allowed to access private members. To work around this, the proxies can be unwrapped with noUseAfterFree.unwrap():

import noUseAfterFree from "@sirpepe/no-use-after-free";

class Example {
  #x = 1;

  static readX(instance) {
    return instance.#x;
  }

  [Symbol.dispose]() {
    console.log("Cleaning up");
  }
}

using instance1 = new Example();
Example.readX(instance1); // OK

using instance2 = noUseAfterFree(new Example());
Example.readX(noUseAfterFree.unwrap(instance3)); // OK

When in doubt, just add noUseAfterFree.unwrap(). Objects that are not proxies created by noUseAfterFree are returned as-is.

The way to think about this is that the party responsible for creating the wrapper proxy is also responsible for unwrapping. If the wrapper is added in a class constructor or factory function, static methods or associated helper functions should implement unwrapping. noUseAfterFree.unwrap() throws an exception when called with a proxy for an already disposed-of target. This turns the developer experience for users of instances into a no-brainer:

import noUseAfterFree from "@sirpepe/no-use-after-free";

class Example {
  #x = 1;

  // Auto-installs noUseAfterFree on instantiation
  constructor() {
    return noUseAfterFree(this);
  }

  // Manually unwraps to be able to access #x
  // Throws if the object has already been disposed of
  static readX(instance) {
    return noUseAfterFree.unwrap(instance).#x;
  }

  [Symbol.dispose]() {
    /* ... */
  }
}

const instance = (function () {
  using instance = new Example();
  console.log(Example.readX(instance)); // works, logs 1
  return instance;
})();

// Fails: object has already been disposed of
console.log(Example.readX(instance));

Remember: if you use the class decorator syntax, the above edge case does not apply. When used as a decorator, the main function wraps the entire class into a proxy that turns instances into proxies and auto-unwraps proxies in arguments to static methods.

Cursed edge cases

Private properties on class instances are not really properties, but more like using a WeakMap to associate data with objects. As a consequence, one can easily engineer a way to access private properties on objects through an already-revoked proxy, bypassing this library's protections:

import assert from "node:assert";
import noUseAfterFree from "@sirpepe/no-use-after-free";

// Note that the decorator, because it only gets applied to the base class,
// can't do anything to subclasses. It does wrap "this" in a proxy but can't
// touch subclasses static methods.
@noUseAfterFree
class Base {
  [Symbol.dispose]() {}
}

class Example extends Base {
  // This adds an actual property on the instance. This means that this
  // property can't be accessed once the proxy is revoked after
  // Symbol.dispose() has run to completion.
  notSecret = 42;

  // This does actually NOT add a property on the instance, but works more
  // like special of variable that's only valid inside the "scope" defined
  // by the Example class
  #secret = 23;

  static readSecret(instance: Example) {
    return instance.#secret;
  }

  static readNotSecret(instance: Example) {
    return instance.notSecret;
  }
}
// "instance" is immediately revoked once it gets returned from the IIFE
const instance = (function () {
  using ex = new Example();
  assert.equal(Example.readSecret(ex), 23); // works as expected
  assert.equal(Example.readNotSecret(ex), 42); // works as expected
  return ex;
})();
// Fails; instance has been revoked
assert.throws(() => instance.notSecret);
// Fails; instance has been revoked
assert.throws(() => Example.readNotSecret(instance));
// !!! DOES NOT FAIL !!! Reading the private field does not involve an
// actual property read from "instance", so it can't be prevented by the
// library.
assert.throws(() => Example.readSecret(instance)); // assertion error

Welp. That's just how JavaScript is ¯\_(ツ)_/¯