mst

Oh my word.

    const defer = f => ({ [Symbol.dispose]: f })

    using defer(() => cleanup())
That only just occurred to me. To everybody else who finds it completely obvious, "well done" but it seemed worthy of mention nonetheless.
show comments
vaylian

I understand that JavaScript needs to maintain backwards compatibility, but the syntax

[Symbol.dispose]()

is very weird in my eyes. This looks like an array which is called like a function and the array contains a method-handle.

What is this syntax called? I would like to learn more about it.

show comments
xg15

This is a great idea, but:

> Integration of [Symbol.dispose] and [Symbol.asyncDispose] in web APIs like streams may happen in the future, so developers do not have to write the manual wrapper object.

So for the foreseeable future, you have a situation where some APIs and libraries support the feature, but others - the majority - don't.

So you can either write your code as a complicated mix of "using" directives and try/catch blocks - or you can just ignore the feature and use try/catch for everything, which will result in code that is far easier to understand.

I fear this feature has a high risk of getting a "not practically usable" reputation (because right now that's what it is) which will be difficult to undo even when the feature eventually has enough support to be usable.

Which would be a real shame, as it does solve a real problem and the design itself looks well thought out.

show comments
havkom

Reminds me of C#.. IDisposible and IAsyncDisposible in C# helps a lot to write good mechanisms for things that should actually be abstracted in a nice way (such as locks handling, queue mechanisms, temporary scopes for impersonation, etc).

show comments
qudat

Resource management, especially when lexical scoping is a feature, is why some of us have been working on bringing structured concurrency to JS: https://bower.sh/why-structured-concurrency

Library that leverages structured concurrency: https://frontside.com/effection

mst

If you want to play with this, Bun 1.0.23+ seems to already have support: https://github.com/oven-sh/bun/discussions/4325

TekMol

Their first example is about having to have a try/finally block in a function like this:

    function processData(response) {
        const reader = response.body.getReader();
        try {
            reader.read()
        } finally {
            reader.releaseLock();
        }
    }
So that the read lock is lifted even if reader.read() throws an error.

Does this only hold for long running processes? In a browser environment or in a cli script that terminates when an error is thrown, would the lock be lifted when the process exits?

show comments
qprofyeh

Can someone explain why they didn’t go with (anonymous) class destructors? Or something other than a Symbol as special object key. Especially when there are two Symbols (different one for asynchronous) which makes it a leaky abstraction, no?

show comments
0xCE0

I don't understand how somebody can code like this and reason/control anything about the program execution :)

async (() => (e) { try { await doSomething(); while (!done) { ({ done, value } = await reader.read()); } promise .then(goodA, badA) .then(goodB, badB) .catch((err) => { console.error(err); } catch { } finally { using stack = new DisposableStack(); stack.defer(() => console.log("done.")); } });

show comments
90s_dev

When I discovered this feature, I looked everywhere in my codebases for a place to use it. Turns out most JS APIs, whether Web or Node.js, just don't need it, since they auto-close your resources for you. The few times I did call .close() used callbacks and would have been less clean/intuitive/correct to rewrite as scoped. I haven't yet been able to clean up even one line of code with this feature :(

show comments
bingemaker

Unsure if this is inspired from C++ RAII. RAII looks very elegant.

`[Symbol.dispose]()` threw me off

show comments
the_mitsuhiko

This is very useful for resource management of WASM types which might have different memory backing.

show comments
davidmurdoch

I tried to write a `using` utility for JS a few years ago: https://gist.github.com/davidmurdoch/dc37781b0200a2892577363...

It's not very ergonomic so I never tried to use it anywhere.

show comments
ivan_gammel

Is it the same as try with resources in Java?

   try(var reader = getReader()) {
       // do read
   } // auto-close
show comments
creata

This seems error-prone, for at least two reasons:

* If you accidentally use `let` or `const` instead of `using`, everything will work but silently leak resources.

* Objects that contain resources need to manually define `dispose` and call it on their children. Forgetting to do so will lead to resource leaks.

It looks like defer dressed up to resemble RAII.

show comments
taylorallred

Ngl, I was hoping “resources” was referring to memory.

show comments
CreepGin

Need to dig into this more, but I built OneJS [1] (kinda like React Native but for Unity), and at first glance this looks perfect for us(?). Seems to be super handy for Unity where you've got meshes, RenderTextures, ComputeBuffers, and NativeContainers allocations that all need proper disposal outside of JS. Forcing disposal at lexical scopes, we can probs keep memory more stable during long Editor sessions or when hot-reloading a lot.

[1] https://github.com/Singtaa/OneJS

jmull

I would have preferred "defer", but "using" is a lot better than nothing.

show comments
russellbeattie

Maybe it's just me, but [Symbol.dispose]() seems like a really hacky way to add that functionality to an Object. Here's their example:

    using readerResource = {
        reader: response.body.getReader(),
        [Symbol.dispose]() {
            this.reader.releaseLock();
        },
    };
First, I had to refresh my memory on the new object definition shorthand: In short, you can use a variable or expression to define a key name by using brackets, like: let key = "foo"; { [key]: "bar"}, and secondly you don't have to write { "baz" : function(p) { ... } }, you can instead write { baz(p) {...} }. OK, got it.

So, if I'm looking at the above example correctly, they're implementing what is essentially an Interface-based definition of a new "resource" object. (If it walks like a duck, and quacks...)

To make a "resource", you'll tack on a new magical method to your POJO, identified not with a standard name (like Object.constructor() or Object.__proto__), but with a name that is a result of whatever "Symbol.dispose" evaluates to. Thus the above definition of { [Symbol.dispose]() {...} }, which apparently the "using" keyword will call when the object goes out of scope.

Do I understand that all correctly?

I'd think the proper JavaScript way to do this would be to either make a new object specific modifier keyword like the way getters and setters work, or to create a new global object named "Resource" which has the needed method prototypes that can be overwritten.

Using Symbol is just weird. Disposing a resource has nothing to do with Symbol's core purpose of creating unique identifiers. Plus it looks fugly and is definitely confusing.

Is there another example of an arbitrary method name being called by a keyword? It's not a function parameter like async/await uses to return a Promise, it's just a random method tacked on to an Object using a Symbol to define the name of it. Weird!

Maybe I'm missing something.

show comments
smashah

I would like to petition JSLand to please let go of the word "use" and all of its derivatives. Cool feature though, looking forward to using (smh) it.

show comments
roschdal

JavaScript new features: segmentation faults, memory leaks, memory corruption and core dumps.

show comments
demarq

So… drop

show comments
sylware

Still implemented with the super villain language, c++?

show comments
bvrmn

Context managers: exist.

JS: drop but we couldn't occupy a possibly taken name, Symbol for the win!

It's hilariously awkward.

show comments
sufianrhazi

This is a great upcoming feature, I wrote some practical advice (a realistic example, how to use it with TypeScript/vite/eslint/neovim/etc…) about it a few months ago here: https://abstract.properties/explicit-resource-management-is-...

spelley

This looks most similar to golang’s defer. It runs cleanup code when leaving the current scope.

It differs from try/finally, c# “using,” and Java try-with-resources in that it doesn’t require the to-be-disposed object to be declared at the start of the scope (although doing so arguably makes code easier to understand).

It differs from some sort of destructor in that the dispose call is tied to scope, not object lifecycle. Objects may outlive the scope if there are other references, and so these are different.

If you like golang’s defer then you might like this.

show comments