JavaScript's New Superpower: Explicit Resource Management
329 points
10 months ago
| 32 comments
| v8.dev
| HN
nayuki
10 months ago
[-]
This proposal reeks of "What color is your function?" https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... . The distinction between sync functions and async functions keeps intruding into every feature. As we can see here, there are Symbol.dispose and Symbol.asyncDispose, and DisposableStack and AsyncDisposableStack.

I am so glad that Java decided to go down the path of virtual threads (JEP 444, JDK 21, Sep 2023). They decided to put some complexity into the JVM in order to spare application developers, library writers, and human debuggers from even more complexity.

reply
jupp0r
10 months ago
[-]
I disagree. Hiding async makes reasoning about code harder and not easier. I want to know whether disposal is async and potentially affected by network outages, etc.
reply
swyx
10 months ago
[-]
look into how React Suspense hides asynchrony (by using fibers). its very commingled with nextjs but the original ideas of why react suspense doesnt use promises (sebmarkbage had a github issue about it) is very compelling
reply
notnullorvoid
10 months ago
[-]
Compelling? It's freaking terrible, instead of pausing execution to be resumed when a promise is resolved they throw execution and when the promise is resolved the whole execution runs again, and potential throws again if it hits another promise, and so on. It's a hacked solution due to the use of a global to keep track of the rendering context to associate with hook calls, so it all needs to happen synchronously. If they had passed a context value along with props to the function components then they could have had async/await and generator components.
reply
throw10920
10 months ago
[-]
This is fallacious. You could use the same logic to argue that we should encode the type of every argument and return value of a function into the function signature, and have to explicitly write it out by hand at every call site, for the same reason:

x = number:foo(number:x, string:y)

It's absurd. The type system should responsible for keeping track of the async status of the function, and you should get that when hovering over the function in your IDE. It does not belong in the syntax any more than the above does and it's an absolutely terrible reason to duplicate all of your functions and introduce these huge headaches.

reply
paulddraper
10 months ago
[-]
To be clear......

This does not introduce function coloring.

You are mearly pointing out the effects of pre-existing function coloring, in that there are two related symbols Symbol.dispose and Symobl.asyncDispose.

Just like there is Symbol.iterator and Symbol.asyncIterator.

reply
bla3
10 months ago
[-]
That's a critique of `async`, not of `using` though, right? This doesn't seem to make functions more colored than they already are as far as I understand.
reply
anon291
10 months ago
[-]
This is because normal execution and async functions form distinct closed Cartesian categories in which the normal execution category is directly embeddable in the async one.

All functions have color (i.e. particular categories in which they can be expressed) but only some languages make it explicit. It's a language design choice, but categories are extremely powerful and applicable beyond just threading. Plus, Java and thread based approaches have to deal with synchronization which is ... Difficult.

(JavaScript restricts itself to monadic categories and more specifically to those expressible via call with continuation essentially)

reply
rtpg
10 months ago
[-]
It’s so annoying that right now the state of the art is essentially “just write all of your code as async because even sync callers could just spin up a one-off event loop in the worst case” in most languages.

The only language I know that navigates this issue well is Purescript, because you can write code that targets Eff (sync effects) or Aff (async effects) and at call time decide.

Structured concurrency is wonderful, but my impression is we’re doing all this syntactic work not to get structured concurrency, but mostly to have, like, multiple top-level request handlers in our server. Embarassingly parallel work!

reply
dingi
10 months ago
[-]
Indeed. Virtual threads, structured concurrency and scoped values are great features.
reply
timewizard
10 months ago
[-]
In plain javascript it's not a problem. Types are ducked so if you receive a result or a promise it doesn't matter. You can functionally work around the "color problem" using this dynamism.

It's only when you do something wacky like try to add a whole type system to a fully duck typed language that you run into problems with this. Or if you make the mistake of copying this async/await mechanism and then hamfistedly shove it into a compiled language.

reply
rtpg
10 months ago
[-]
Typescript has no problem with typing your scenario (or at least nothing that isn’t present in “plain” JavaScript… what if your value is a promise?)

And compiled languages don’t have more trouble with this than JavaScript. Or rather, Javascript doesn’t have less issues on this front. The color issue is an issue at the syntactic level!

reply
timewizard
10 months ago
[-]
Except you can await on non-Promise objects which just returns the original object. Most other typed languages do not appear have this convenience. C# (the apparent source of the color rant) does not. It sets JavaScript apart.

Likewise Promise.resolve() on a promise object just returns the original promise. You can color and uncolor things with far less effort or knowledge of the actual type.

reply
jakelazaroff
10 months ago
[-]
Awaiting a non-promise doesn’t make it synchronous, though — it still executes any subsequent lines in a microtask.

Try running this code. It’ll print “bar” first and then “foo”, even though the function only awaits a string literal and the caller doesn’t await anything at all.

  const foo = async () => console.log(await "foo");

  foo();
  console.log("bar");
reply
neonsunset
10 months ago
[-]
In C# nothing stops you from doing `var t = Task.Run(() => ExpensiveButSynchronous());` and then `await`ing it later. Not that uncommon for firing off known long operations to have some other threadpool thread deal with it.

Unless you literally mean awaiting non-awaitable type which...just doesn't make sense in any statically typed language?

reply
dang
10 months ago
[-]
reply
vips7L
10 months ago
[-]
Beyond happy Java made that decision as well.
reply
olalonde
10 months ago
[-]
I'm not sure how it's implemented in the JVM, but in general, multithreading is notoriously difficult to reason about. Entire books are devoted to the pitfalls (race conditions, deadlocks, livelocks, starvation, memory visibility issues, and more). Compared to that, single threaded async programming is a walk in the park. I’d rather deal with "function color" than try to debug a heisenbug in a multithreaded app.
reply
pton_xd
10 months ago
[-]
Share-nothing via message passing ala Erlang makes multithreading much more tractable. I daresay it's enjoyable.

Async is a decent syntax for simple tasks but that simplicity falls apart when composing larger structures and dealing with error handling and whatnot. I find it more difficult to understand what's going on compared to explicit threading.

reply
olalonde
10 months ago
[-]
> Async is a decent syntax for simple tasks but that simplicity falls apart when composing larger structures and dealing with error handling and whatnot.

Do you have a concrete example? It has just never really been an issue for me since async/await (callback hell was a thing though).

reply
vips7L
10 months ago
[-]
You don’t know how it works and are commenting on it?? Peak HN.
reply
olalonde
10 months ago
[-]
Multithreading works similarly across most programming languages, and that was my assumption here as well. Your comment made me second-guess myself a bit, so I went back and checked the spec. Turns out it is indeed just standard multithreading, as I originally thought.
reply
afiori
10 months ago
[-]
The implementation and the observable behaviour are two different things
reply
mst
10 months ago
[-]
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.
reply
masklinn
10 months ago
[-]
Note that depending on use case, it may be preferrable to use `DisposableStack` and `AsyncDisposableStack` which are part of the `using` proposal and have built-in support for callback registration.

This is notably necessary for scope-bridging and conditional registration as `using` is block-scoped so

    if (condition) {
        using x = { [Symbol.dispose]: cleanup }
    } // cleanup is called here
But because `using` is a variant of `const` which requires an initialisation value which it registers immediately this will fail:

    using x; // SyntaxError: using missing initialiser
    if (condition) {
        x = { [Symbol.dispose]: cleanup };
    }
and so will this:

    using x = { [Symbol.dispose]() {} };
    if (condition) {
        // TypeError: assignment to using variable
        x = { [Symbol.dispose]: cleanup }
    }
Instead, you'd write:

    using x = new DisposableStack;
    if (condition) {
        x.defer(cleanup)
    }
Similarly if you want to acquire a resource in a block (conditionally or not) but want the cleanup to happen at the function level, you'd create a stack at the function toplevel then add your disposables or callbacks to it as you go.
reply
MrResearcher
10 months ago
[-]
What is the purpose of DisposableStack.move()? Can it be used to transfer the collected .defer() callbacks entirely out of the current scope, e.g. up the call stack? Probably would be easier to pass DisposableStack as an argument to stack all .defer() callbacks in the caller's context?
reply
bakkoting
10 months ago
[-]
Yup, or to transfer them anywhere else. One use case is for classes which allocate resources in their constructor:

    class Connector {
      constructor() {
        using stack = new DisposableStack;
        
        // Foo and Bar are both disposable
        this.foo = stack.use(new Foo());
        this.bar = stack.use(new Bar());
        this.stack = stack.move();
      }
      
      [Symbol.dispose]() {
        this.stack.dispose();
      }
    }
In this example you want to ensure that if the constructor errors partway through then any resources already allocated get cleaned up, but if it completes successfully then resources should only get cleaned up once the instance itself gets cleaned up.
reply
masklinn
10 months ago
[-]
> Probably would be easier to pass DisposableStack as an argument to stack all .defer() callbacks in the caller's context?

The problem in that case if if the current function can acquire disposables then error:

    function thing(stack) {
        const f = stack.use(new File(...));
        const g = stack.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff
        return someObject(f, g);
    }
rather than be released on exit, the files will only be released when the parent decides to dispose of its stack.

So what you do instead is use a local stack, and before returning successful control you `move` the disposables from the local stack to the parents', which avoids temporal holes:

    function thing(stack) {
        const local = new DisposableStack;
        const f = local.use(new File(...));
        const g = local.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff

        stack.use(local.move());
        return someObject(f, g);
    }
Although in that case you would probably `move` the stack into `someObject` itself as it takes ownership of the disposables, and have the caller `using` that:

    function thing() {
        const local = new DisposableStack;
        const f = local.use(new File(...));
        const g = local.use(new File(...));
        if (something) {
            throw new Error
        }
        // do more stuff

        return someObject(local.move(), f, g);
    }
In essence, `DisposableStack#move` is a way to emulate RAII's lifetime-based resource management, or the error-only defers some languages have.
reply
masklinn
10 months ago
[-]
(const local should be using local in the snippets).
reply
MrJohz
10 months ago
[-]
I wrote about this last year, it's one of my favourite bits of this spec: https://jonathan-frere.com/posts/disposables-in-javascript/#...

TL;DR: the problem if you just pass the DisposableStack that you're working with is that it's either a `using` variable (in which case it will be disposed automatically when your function finishes, even if you've not actually finished with the stack), or it isn't (in which case if an error gets thrown while setting up the stack, the resources won't be disposed of properly).

`.move()` allows you to create a DisposableStack that's a kind of sacrificial lamb: if something goes wrong, it'll dispose of all of its contents automatically, but if nothing goes wrong, you can empty it and pass the contents somewhere else as a safe operation, and then let it get disposed whenever.

reply
mattlondon
10 months ago
[-]
Just like golang. Nice.
reply
xg15
10 months ago
[-]
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.

reply
jitl
10 months ago
[-]
This is the situation in JavaScript for the last 15 years: new language features come first to compilers like Babel, then to the language spec, and then finally are adopted for stable APIs in conservative NPM packages and in the browser. The process from "it shows up as a compiler plugin" to "it's adopted by some browser API" can often be like 3-4 years; and even after it's available in "evergreen" browsers, you still need to have either polyfills or a few more years of waiting for it to be guaranteed available on older end-user devices.

Developers are quite used to writing small wrappers around web APIs anyways since improvement to them comes very slowly, and a small wrapper is often a lesser evil compared to polyfills; or the browser API is just annoying on the typical use path so of course you want something a little different.

At least, I personally have never seen a new langauge feature that seems useful and thought to myself "wow this is going to be hard to use"

reply
MrJohz
10 months ago
[-]
In practice, a lot of stuff has already implemented this using forwards-compatible polyfills. Most of the backend NodeJS ecosystem, for example, already supports a lot of this, and you have been able to use this feature quite effectively for some time (with a transpiler to handle the syntax). In fact, I gave a couple of talks about this feature last year, and while researching for them, I was amazed by how many APIs in NodeJS itself or in common libraries already supported Symbol.dispose, even if the `using` syntax wasn't implemented anywhere.

I suspect it's going to be less common in frontend code, because frontend code normally has its own lifecycle/cleanup management systems, but I can imagine it still being useful in a few places. I'd also like to see a few more testing libraries implement these symbols. But I suspect, due to the prevalence of support in backend code, that will all come with time.

reply
bakkoting
10 months ago
[-]
For APIs which don't support this, you can still use `using` by using DisposableStack:

    using disposer = new DisposableStack;
    const resource = disposer.adopt(new Resource, r => r.close());
This is still simpler than try/catch, especially if you have multiple resources, so it can be adopted as soon as your runtime supports the new syntax, without needing to wait for existing resources to update.
reply
berkes
10 months ago
[-]
Isn't this typically solved with polyfills in the JavaScript world?
reply
mst
10 months ago
[-]
I regularly add Symbol based features to JS libraries I'm using (named methods are riskier, of course)

    import { SomeStreamClass as SomeStreamClass_ } from "some/library"
    export class SomeStreamClass extends SomeStreamClass_ {
      [someSymbol] (...) { ... }
      ...
    }
I have not blown my foot off yet with this approach but, uh, no warranty, express or implied.

It's been working excellently for me so far though.

reply
sroussey
10 months ago
[-]
Much nicer than just adding your symbol method to the original class. :p
reply
mst
10 months ago
[-]
13 days late but for posterity:

Yes. Not Wanting To Do That was the motivating factor for coming up with this approach :D

reply
berkes
10 months ago
[-]
I guess it could be improved with a simple check if SomeStreamClass_ already has someSymbol and then raise an exception, log a warning or some such.
reply
mst
10 months ago
[-]
8 days late but for posterity:

So far I've only ever been using a private symbol that only exists within the codebase in question (and is then exported to other parts of said codebase as required).

If I ever decide to generalise the approach a bit, I'll hopefully remember to do precisely what you describe.

Possibly with the addition of providing an "I am overriding this deliberately" flag that blows up if it doesn't already have said symbol.

But for the moment, the maximally dumbass approach in my original post is DTRT for me so far.

reply
spion
10 months ago
[-]
This is why TC39 needs to work on fundamental language features like protocols. In Rust, you can define a new trait and impl it for existing types. This still has flaws (orphan rule prevents issues but causes bloat) but it would definitely be easier in a dynamic language with unique symbol capabilies to still come up with something.
reply
jitl
10 months ago
[-]
Dynamic languages don't need protocols. If you want to make an existing object "conform to AsyncDisposable", you:

    function DisposablImageBitmap(bitmap) {
      bitmap[Symbol.dispose] ??= () => bitmap.close()
      return bitmap
    }
    
    using bitmap = DisposableObserver(createImageBitmap(image))
Or if you want to ensure all ImageBitmap conform to Disposable:

    ImageBitmap.prototype[Symbol.dispose] = function() { this.close() }
But this does leak the "trait conformance" globally; it's unsafe because we don't know if some other code wants their implementation of dispose injected to this class, if we're fighting, if some key iteration is going to get confused, etc...

How would a protocol work here? To say something like "oh in this file or scope, `ImageBitmap.prototype[Symbol.dispose]` should be value `x` - but it should be the usual `undefined` outside this scope"?

reply
spion
10 months ago
[-]
You could potentially use the module system to bring protocol implementations into scope. This could finally solve the monkey-patching problem. But its a fairly novel idea, TC39 are risk-averse, browser-side are feature-averse and the language has complexities that create issues with most of the more interesting ideas.
reply
someothherguyy
10 months ago
[-]
Isn't disconnecting a resize observer a poor example of this feature?
reply
jitl
10 months ago
[-]
I couldn't come up with a reasonable one off the top of my head, but it's for illustration - please swap in a better web api in your mind

(edit: changed to ImageBitmap)

reply
TheRealPomax
10 months ago
[-]
> So for the foreseeable future, you have a situation where some APIs and libraries support the feature, but others - the majority - don't.

Welcome to the web. This has pretty much been the case since JavaScript 1.1 created the situation where existing code used shims for things we wanted, and newer code didn't because it had become part of the language.

reply
havkom
10 months ago
[-]
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).
reply
pwdisswordfishz
10 months ago
[-]
reply
spankalee
10 months ago
[-]
That looks like a lot of very reasonable responses to me.
reply
Zacru
10 months ago
[-]
reply
masklinn
10 months ago
[-]
It's basically lifted from C#'s, the original proposal makes no secret of it and cites all of Python's context managers, Java's try with resources, C#'s using statements, and C#'s using declarations. And `using` being the keyword and `dispose` the hook method is a pretty big hint.
reply
vaylian
10 months ago
[-]
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.

reply
zdragnar
10 months ago
[-]
Dynamic keys (square brackets on the left hand side in an object literal) have been around for nearly 10 years, if memory serves.

https://www.samanthaming.com/tidbits/37-dynamic-property-nam...

Also in the example is method shorthand:

https://www.samanthaming.com/tidbits/5-concise-method-syntax...

Since symbols cannot be referred to by strings, you can combine the two.

Basically, there isn't any new syntax here.

reply
a4isms
10 months ago
[-]
Yes, this will be familiar to people creating objects or classes that are intended to represent iterable collections. You do the same dynamic key syntax with a class declaration or object literal, but use `Symbol.iterator` as the well-known symbol for the method.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

reply
mceachen
10 months ago
[-]
Other posters correctly described _what_ this is, but I didn't see anyone answer _why_.

Using a Symbol as the method name disambiguates this method from any previously-defined methods.

In other words, by using a Symbol for the method name (and not using a string), it's impossible to "name collide" on this new API, which would accidentally mark a class as disposable.

reply
whizzter
10 months ago
[-]
This is the most important reason!
reply
MrJohz
10 months ago
[-]
Dynamic property access perhaps?

The premise is that you can always access an object's properties using indexing syntax as well as the normal dot syntax. So `object.foo` is the equivalent of `object["foo"]` or `object["f" + "o" + "o"]` (because the value inside the square brackets can be any expression). And if `object.foo` is a method, you can do `object.foo()` or `object ["foo"]()` or whatever else as well.

Normally, the key expression will always be coerced to a string, so if you did `object[2]`, this would be the equivalent of object["2"]. But there is an exception for symbols, which are a kind of unique object that is always compared by reference. Symbols can be used as keys just as they are, so if you do something like

    const obj = {}
    obj.foo = "bar"
    obj[Symbol("foo")] = "bar"
    console.log(obj)
You should see in the console that this object has a special key that is a symbol, as well as the normal "foo" attribute.

The last piece of the puzzle is that there are certain "well known symbols" that are mostly used for extending an object's behaviour, a bit like __dunder__ methods in Python. Symbol.dispose is one of these - it's a symbol that is globally accessible and always means the same thing, and can be used to define some new functionality without breaking backwards compatibility.

I hope that helps, feel free to ask more questions.

reply
cluckindan
10 months ago
[-]
It’s not that, it’s a dynamic key in an object literal.

    const key = "foo";
    const obj = { [key]: "bar" };
    console.log(obj.foo); // prints "bar"
reply
MrJohz
10 months ago
[-]
That's also possible, and it's common when using this pattern, but the specific syntax in the original question was I believe property access, and not part of a property literal. I didn't bring that up because I thought my comment was long enough and I wanted to explain that specific syntax. But yeah, you also have this syntax to set properties in object literals, and a similar syntax in classes.
reply
homebrewer
10 months ago
[-]
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Someone more knowledgeable will join in soon, but I'm pretty sure it was derived from:

  const x = { age: 42 };
  x[Symbol.name] = "joe"; // <--- this
so it makes a lot of sense.
reply
ff2400t
10 months ago
[-]
This syntax has been used for quite some time. JavaScript iterator use the same syntax and they been part of JavaScript for almost a decade now.
reply
90s_dev
10 months ago
[-]

  const o = {}
  o["foo"] = function(){}
  o["foo"]()
  let key = "foo"
  o[key]()
  key = Symbol.dispose ?? Symbol.for('dispose')
  o[key]()
  o[Symbol.dispose]()
reply
paulddraper
10 months ago
[-]
That's a notational reference to a functional.

If the code is

  obj.function()
they are notating it as `function()`.

If the code is

  obj[Symbol.dispose]()
they are notating it as `[Symbol.dispose]()`.

Symbol.dispose is a symbol key.

reply
vaylian
10 months ago
[-]
> If the code is

> obj[Symbol.dispose]()

> they are notating it as `[Symbol.dispose]()`.

So

`obj[Symbol.dispose]()` is the same as `[Symbol.dispose]()`? That doesn't seem right, because we might also have `obj2` or `obj3`. How does JavaScript know that `[Symbol.dispose]()` refers to a specific object?

reply
masklinn
10 months ago
[-]
[Symbol.dispose] is a way of creating an entry whose key is the value of the expression Symbol.dispose in the same way obj[Symbol.dispose] is a way of accessing it.

The parens are just the method definition shorthand, so it’s a shorter way of writing

    [Symbol.dispose]: function()
Bracketing was introduced because Javascript was originally defined to use bare keys so

    foo: bar
Defines an entry with the key `”foo”`, rather than an entry whose key is the value for the variable `foo`. Thus to get the latter you use

    [foo]: bar
reply
Kalabasa
10 months ago
[-]
Object property access i guess. Like

myObj["myProperty"]

If it's a function then it could be invoked,

myObj["myProperty"]()

If the key was a symbol,

myObj[theSymbol]()

reply
TheRealPomax
10 months ago
[-]
pretty sure they were asking about the dynamic property name, { [thing]: ... }
reply
qudat
10 months ago
[-]
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

reply
0xCE0
10 months ago
[-]
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.")); } });

reply
snickerbockers
10 months ago
[-]
That's the neat part, you don't. 90% percent of webdev is "upgrading" things in ways nobody asked for or appreciates because it's just taken for granted that your codebase will grow mold or something if it isn't stirred often enough and the other 10% of the work is fixing legitimate problems resulting from the first 90%. Of course, no probability is ever actually 1.0 so there will be rare occasions that you need to understand something that ChatGP-err, sorry my bad, i meant to say "something that you" wrote more than a year ago you suggest to your boss that this bug should be preserved until next time there's a new hire because it would make a great "jumping-on" point and until then the users will still be able to get work done by using the recommended work-around, whic his installing windows XP Pirate Edition onto a VM and using IE6 to get into the legacy-portal that somehow inexplicably still exists 20 years after the corporate merger that was supposed to make it obsolete.
reply
lioeters
10 months ago
[-]
I fell off your train of thought about halfway through, but I agree with the main point that there's way too much unnecessary churn in the web dev world, 90% is about right. Just busy work, changing APIs, forcing new and untested paradigms onto library users, major version upgrades that expect everyone to rewrite their code..

Intentionally or unconsciously, much of the work is about ensuring there will always be demand for more work. Or else there's a risk of naturally falling apart over time. Why would you build it that way!?

reply
exe34
10 months ago
[-]
You wrote out loud what I've been thinking quietly.
reply
gchamonlive
10 months ago
[-]
From the lack of punctuation I think you can also rap it out loud.
reply
snickerbockers
10 months ago
[-]
reply
gchamonlive
10 months ago
[-]
This is genius! For the first time in my life my pedantry got rewarded.
reply
eastbound
10 months ago
[-]
Oh, we must upgrade, because of vulnerabilities. All the vulnerabilities found in 90% of this moot code.

Ok, point taken.

reply
the_arun
10 months ago
[-]
Your paragraph is as complicated as the code we create over time. Is this your point? Then I take it.
reply
90s_dev
10 months ago
[-]
For starters, your code is so full of serious syntax errors that in some places it's not even close to valid JavaScript. This is my best guess reconstruction:

    (async (e) => {
      await doSomething()
      while (!done) {
        ({ done, value }) = await reader.read()
      }

      promise
        .then(goodA, badA)
        .then(goodB, badB)
        .catch(err => console.log(err))
        .finally(() => {
          using stack = new DisposableStack()
          stack.defer(() => console.log('done.'))
        })
    })()
But more importantly, this isn't even close to anything a reasonable JS dev would ever write.

1. It's not typical to mix await and while(!done), I can't imagine what library actually needs this. You usually use one or the other, and it's almost always just await:

    await doSomething()
    const value = await readFully(reader)
2. If you're already inside an Async IIFE, you don't need promise chains. Just await the stuff as needed, unless promise chains make the code shorter and cleaner, e.g.:

    const json = await fetch(url).then(r => r.json())
3. Well designed JS libraries don't usually stack promise handlers like the {good,bad}{A,B} functions you implied. You usually just write code and have a top level exception handler:

    using stack = new DisposableStack()
    stack.defer(() => console.log('done.'))
    try {
      const goodA = await promise
      const goodB = await goodA
      const goodC = await goodB
      return goodC
    }
    catch(e) {
      myLogErr(e)
    }
    // finally isn't needed, that's the whole point of DisposableStack
4. We don't usually need AIIFEs anymore, so the outer layer can just go away.
reply
TheRealPomax
10 months ago
[-]
note about that await block: "await" will await the _entire_ return, so if "promise" returns another promise ("goodA") which in turn also returns a promise ("goodB"), which in turn returns _another_ promise that ends up resolving as the non-promise value "goodC", then "await promise" just... gets you "goodC", directly.

The "example code" (if we can call it that) just used goodA and goodB because it tried to make things look crazy, by writing complete nonsense: none of that is necessary, we can just use a single, awaiting return:

    try {
      return await promise;
    } catch(e) {
      handleYourExceptions(e);
    }
Done. "await" waits until whatever it's working with is no longer a promise, automatically either resolving the entire chain, or if the chain throws, moving us over to the exception catching part of our code.
reply
gavinray
10 months ago
[-]
By programming in the language for a living and being familiar with the semantics of the language's keywords -- likely the same way anyone else understands their preferred language?

People write Haskell for a living, after all.

reply
johnisgood
10 months ago
[-]
And Lisp, and Forth... :D
reply
90s_dev
10 months ago
[-]
Lisp I can understand to some degree... but writing Forth for a living? I know about 20-30 languages, but that one is Greek to me.
reply
johnisgood
10 months ago
[-]
Stack-based or concatenative languages can be difficult to understand, but as with anything, you may / could get used to it. :)

I prefer Factor[1] over Forth, however. Maybe you'll like it!

[1] https://factorcode.org/

reply
90s_dev
10 months ago
[-]
It's not that they're hard to understand, it's that they're much denser. From Factor's examples page:

> 2 3 + 4 * .

There's a lot more there to mentally parse than:

> (2 + 3) * 4

It's the same as when Rob Pike decries syntax highlighting. No, it's very useful to me. I can read much quicker with it.

It's the same principle behind how we use heuristics to much more quickly read words by sipmly looking at the begninnings and ends of each word, and most of the time don't even notice typos.

reply
johnisgood
10 months ago
[-]
Well, I guess it might boil down to how one "thinks"?

Some people prefer:

  2 3 + 4 *
Some other people prefer:

  (* 4 (+ 2 3))
And some other people prefer:

  (2 + 3) * 4
I personally find the last one easier to read or understand, but I have had my fair share of Common Lisp and Factor. :D

Syntax highlighting is useful for many people, including me. I can read much quicker with it, too. I know of some people who write Common Lisp without syntax highlighting though. :)

reply
kazinator
10 months ago
[-]
Forth could be written devilishly where you have this

  2 .... hundreds of words .... +
where the operands of + are 2 and the result produced by the hundreds of words!

Which could also be:

  .... hundreds of words .... 2 +
which would be a lot easier to read!

If you're writing Forth, it likely behooves you to try to adhere to the latter style of chaining where you take everything computed thus far, and apply a small operation to it with a simple operand. Not sure if it's always possible:

  ... complex numerator ... ... complex denominator ... /
Now find the division between the numerator and denominator among all those words.
reply
johnisgood
10 months ago
[-]
> ... complex numerator ... ... complex denominator ... /

Yes, this is why you are supposed to have short words. You should factor out the complex parts into short, self-contained, and descriptively named words, which is going to make your code much easier to read, test, and maintain.

For example:

Instead of:

  a b + c d + * e f + g h + * /
You should probably have:

  : compute-numerator   a b + c d + * ;
  : compute-denominator e f + g h + * ;
  : compute-ratio       compute-numerator compute-denominator / ;
Most (if not all) Forth books mention this as well.
reply
kazinator
10 months ago
[-]
Don't you now have actions in the middle of the computation that are putting names into a global dictionary? I'd at least give them names like tmp-numerator to put them into a namespace of local/temporary functions, and then "forget" them immediately after the computaton that references them.

What's the compiled version of : compute-numerator a b + c d + * ; look like? I imagine at the very least that there has to be a call to some run-time support routine to insert a compiled thunk under a name into the dictionary.

reply
johnisgood
10 months ago
[-]
Yes, defining words like "compute-numerator" does add entries to the dictionary, but that happens entirely at compile time. Forth doesn't insert a "compiled thunk" at runtime, the word is compiled as a name bound to a sequence of code field addresses (CFAs). When invoked, it's just a jump through the usual inner interpreter. There's no runtime cost for defining the word itself. When you invoke "compute-numerator" at runtime, the inner interpreter simply threads through those CFAs. There's no indirection, JIT, or dynamic thunk creation involved. The only runtime effect is the word being executed when called. All linking is resolved at compile time.

If you're concerned about polluting the global dictionary, a common idiom is (which you already know):

  \ Define and forget immediately if temporary
  : tmp-numerator a b + c d + * ;
  tmp-numerator
  FORGET tmp-numerator
or alternatively, you can isolate temporary definitions in a separate vocabulary:

  VOCABULARY TMP-WORDS
  TMP-WORDS DEFINITIONS

  : numerator   1 2 + 3 4 + * ;
  : denominator 5 6 + 7 8 + * ;

  ONLY FORTH ALSO TMP-WORDS ALSO DEFINITIONS

  : compute-ratio numerator denominator / . ;

  compute-ratio

  ONLY FORTH DEFINITIONS
TL;DR: Defining intermediate words adds entries to the dictionary, but this happens at compile time, not runtime. There's no additional runtime overhead. Naming conventions, FORGET, or vocabularies can mitigate dictionary pollution / clutter, but still, factoring remains the standard idiom in Forth.

Note: In some native code compiling or JIT-based Forth implementations, definitions may generate machine code or runtime objects rather than simple CFA chains I mentioned, but even in these cases, compilation occurs before runtime execution, and no dynamic thunk insertion happens during word calls.

I hope I understood your comment correctly. Please let me know!

reply
notpushkin
10 months ago
[-]
To embed code on HN, add 2 or more spaces at the beginning of each line:

  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.")); }
  });
(indentation preserved as posted by OP – I don't understand how somebody can code like this either :-)
reply
pwdisswordfishz
10 months ago
[-]
Indenting helps.
reply
xg15
10 months ago
[-]
Also, sticking to one style and not mixing all the wildly different approaches to do the same thing.

JS, like HTML has the special property that you effectively cannot make backwards-incompatible changes ever, because that scrappy webshop or router UI that was last updated in the 90s still has to work.

But this means that the language is more like an archeological site with different layers of ruins and a modern city built on top of it. Don't use all the features only because they are available.

reply
wpollock
10 months ago
[-]
There used to be a great book for this, "JavaScript The Good Parts". Is there a well-respected equivalent for JavaScript in 2025?
reply
kubb
10 months ago
[-]
Also practice, programming is hard, but just because one person doesn't understand something, doesn't mean it's impossible or a bad idea.
reply
lukan
10 months ago
[-]
But browsing the web with dev tools open, the amount of error messages on allmost any site implies to me, it is more than one person who doesn't understand something.
reply
exe34
10 months ago
[-]
It's also great for job security if very few people would be able to work on it.
reply
jitl
10 months ago
[-]
you can write horrid code intentionally in any programming language
reply
mrweasel
10 months ago
[-]
It just seems like it's happening way more often in JavaScript, but I've seen absolute horrid and confusing Python as well.

The JavaScript syntax wasn't great to begin with, and as features are added to the language it sort of has to happen within the context of what's possible. It's also becoming a fairly large language, one without a standard library, so things just sort of hang out in a global namespace. It's honestly not to dissimilar to PHP, where the language just grew more and more functions.

As others point out there's also some resemblance to C#. The problem is that parts of the more modern C# is also a confusing mess, unless you're a seasoned C# developer. The new syntax features aren't bad, and developers are obviously going to use them to implement all sorts of things, but if you're new to the language they feel like magical incantations. They are harder to read, harder to follow and doesn't look like anything you know from other language. Nor are they simple enough that you can just sort of accept them and just type the magical number of brackets and silly characters and accept that it somehow work. You frequently have no idea of what you just did or why something works.

I feel like Javascript has reached the point where it's a living language, but because of it's initial implementation and inherit limits, all these great features feel misplaced, bolted on and provides an obstacle for new or less experienced developers. Javascript has become an enterprise language, with all the negative consequences and baggage that entails. It's great that we're not stuck with half a language and we can do more modern stuff, it just means that we can't expect people to easily pick up the language anymore.

reply
neonsunset
10 months ago
[-]
> parts of the more modern C# is also a confusing mess

Do you have any examples?

reply
mrweasel
10 months ago
[-]
For me, personally, heavy use of the => operator (which happens to coinside with my main complaint of a lot JavaScript code and anonymous functions). You can avoid it, but is pretty standard.

Very specifically I also looking into JWT authentication in ASP.NET Core and found the whole thing really tricky to wrap my head around. That's more of a library, but I think many of the usage examples ends up being a bunch of spaghetti code.

reply
neonsunset
10 months ago
[-]
Wait, what? The => operator is just for lambdas, short-form returns and switch expression arms, which is as common as it gets!

Have you never worked with any other language which lets you do these?

    var say = (string s) => Console.WriteLine(s);
or

    struct Lease(DateTime expiration)
    {
        public bool HasExpired => expiration < DateTime.UtcNow;
    }
or

    var num = obj switch
    {
        "hello" => 42,
        1337 => 41,
        Lease { HasExpired: false } => 100,
        _ => 0
    };
You'll see forms of it in practically every (good) modern language. How on Earth is it confusing?

Authentication is generally a difficult subject, it is a weaker(-ish) aspect of (otherwise top of the line) ASP.NET Core. But it has exactly zero to do with C#.

reply
stephenr
10 months ago
[-]
I mean we're talking about a language community where someone created a package to tell if a variable is a number... and it gets used *a lot*.

That JavaScript has progressed so much in some ways and yet is still missing basic things like parameter types is crazy to me.

reply
chrisweekly
10 months ago
[-]
The overwhelming majority of serious work in JS is authored in TypeScript.
reply
stephenr
10 months ago
[-]
That just Sounds like an even stronger argument to add types to the language.
reply
paulddraper
10 months ago
[-]
Yeah.

The hard part is that types are very hard/complex, with many tradeoffs.

A standard with strong backwards compat and is interpreted consistently is hard (see Python).

reply
chrisweekly
10 months ago
[-]
Could be.
reply
cluckindan
10 months ago
[-]
That is a ”no true Scotsman” argument.
reply
chrisweekly
10 months ago
[-]
How so? GP complained about JS lack of types. I pointed out that most JS actually benefits from types, given it's typically authored in TS. No moving goalposts, no "true Scotsman" arg.
reply
cluckindan
10 months ago
[-]
Paraphrasing:

>> JS has progressed but it still lacks types, it seems crazy to do serious programming work in a language that doesn’t have such basic things

> Serious programming work in JS is done in TypeScript

reply
chrisweekly
10 months ago
[-]
My point was agreeing with the GP's underlying point that types matter. Yes it'd be crazy to do serious programming work in JS without types -- that's why ~nobody does that. It's why TypeScript exists and ~everybody doing serious work on JS projects uses it. Their argument was almost a strawman, it refers to a hypothetical situation that doesn't pertain. Serious JS programmers use TS.
reply
cluckindan
10 months ago
[-]
And are vanilla 0-dep 0-build projects just gimmicks and toys?
reply
cluckindan
10 months ago
[-]
Someone needs to start creating leftPad and isOdd type troll packages in Rust just so we can ridicule the hubris.
reply
maleldil
10 months ago
[-]
Done and done:

https://docs.rs/isodd/latest/isodd/

https://docs.rs/leftpad/latest/leftpad/

I bet you can find something similar in all modern package managers.

reply
yencabulator
10 months ago
[-]
The joke falls flat:

> This crate is not used as a dependency in any other crate on crates.io.

https://crates.io/crates/isodd/reverse_dependencies

reply
paulddraper
10 months ago
[-]
> and yet is still missing basic things like parameter types

Like Bash, Python, Ruby?

reply
stephenr
10 months ago
[-]
Python is making steps to add parameter types.

That ruby doesn't have types is also bizarre to me, but Ruby also sees monkey patching code as a positive thing too so I've given up trying to understand its appeal.

reply
TheRealPomax
10 months ago
[-]
It all starts with being well-formatted and having a proper code editor instead of just a textarea on a webpage, so you'd get the many error notices for that code (because it sure as hell isn't valid JS =)

And of course, actually knowing the language you use every minute of the day because that's your job helps, too, so you know to rewrite that nonsense to something normal. Because mixing async/await and .then.catch is ridiculous, and that while loop should never be anywhere near a real code base unless you want to get yelled at for landing code that seems intentionally written to go into a spin loop under not-even-remotely unusual circumstances.

reply
77pt77
10 months ago
[-]
LLMs will do only this and you'll love it.

Maybe not love it, but you really won't have a choice.

reply
mattlondon
10 months ago
[-]
No worse than C++, frankly.
reply
mst
10 months ago
[-]
If you want to play with this, Bun 1.0.23+ seems to already have support: https://github.com/oven-sh/bun/discussions/4325
reply
qprofyeh
10 months ago
[-]
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?
reply
masklinn
10 months ago
[-]
Destructors require deterministic cleanup, which advanced GCs can't do (and really don't want to either from an efficiency perspective). Languages with advanced GCs have "finalizers" called during collection which are thus extremely unreliable (and full of subtle footguns), and are normally only used as a last resort solution for native resources (FFI wrappers).

Hence many either had or ended up growing means of lexical (scope-based) resource cleanup whether,

- HoF-based (smalltalk, haskell, ruby)

- dedicated scope / value hook (python[1], C#, Java)

- callback registration (go, swift)

[1]: Python originally used destructors thanks to a refcounting GC, but the combination of alternate non-refcounted implementations, refcount cycles, and resources like locks not having guards (and not wanting to add those with no clear utility) led to the introduction of context managers

reply
nh2
10 months ago
[-]
What does "HoF" stand for?
reply
masklinn
10 months ago
[-]
higher order function, function taking an other function (/ block).

E.g. in Ruby you can lock/unlock a mutex, but the normal way to do it would be to pass a block to `Mutex#synchronize` which is essentially just

  def synchronize
    lock
    begin
      yield
    ensure
      unlock
    end
  end
and called as:

  lock.synchronize {
    # protected code here
  }
reply
matharmin
10 months ago
[-]
Destructors I other languages are typically used for when the object is garbage collected. That has a whole bunch of associated issues, which is why the pattern is often avoided these days.

The dispose methods on the other hand are called when the variable goes out of scope, which is much more predictable. You can rely on for example a file being closed ot a lock released before your method returns.

JavaScript is already explicit about what is synchronous versus asynchronous everywhere else, and this is no exception. Your method needs to wait for disposing to complete, so if disposing is asynchronous, your method must be asynchronous as well. It does get a bit annoying though that you end up with a double await, as in `await using a = await b()` if you're not used to that syntax.

As for using symbols - that's the same as other functionality added over time, such as iterator. It gives a nice way for the support to be added in a backwards-compatible way. And it's mostly only library authors dealing with the symbols - a typical app developer never has to touch it directly.

reply
senfiaj
10 months ago
[-]
For garbage collected languages destructors cannot be called synchronously in most cases because the VM must make sure that the object is inaccessible first. So it will not work very deterministically, and also will expose the JS VM internals. For that JS already has WeakRef and FinalizationRegistry.

https://waspdev.com/articles/2025-04-09/features-that-every-... https://waspdev.com/articles/2025-04-09/features-that-every-...

But even Mozilla doesn't recommend to use them because they're quite unpredictable and might work differently in different engines.

reply
Garlef
10 months ago
[-]
Because this approach also works for stuff that is not a class instance.
reply
pwdisswordfishz
10 months ago
[-]
There is no such thing as an anonymous property in JavaScript. Your question doesn't make sense. What else could this possibly be?
reply
feverzsj
10 months ago
[-]
Because javascript is uncivilized.
reply
TekMol
10 months ago
[-]
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?

reply
teraflop
10 months ago
[-]
The spec just says that when a block "completes" its execution, however that happens (normal completion, an exception, a break/continue statement, etc.) the disposal must run. This is the same for "using" as it is for "try/finally".

When a process is forcibly terminated, the behavior is inherently outside the scope of the ECMAScript specification, because at that point the interpreter cannot take any further actions.

So what happens depends on what kind of object you're talking about. The example in the article is talking about a "stream" from the web platform streams spec. A stream, in this sense, is a JS object that only exists within a JS interpreter. If the JS interpreter goes away, then it's meaningless to ask whether the lock is locked or unlocked, because the lock no longer exists.

If you were talking about some kind of OS-allocated resource (e.g. allocated memory or file descriptors), then there is generally some kind of OS-provided cleanup when a process terminates, no matter how the termination happens, even if the process itself takes no action. But of course the details are platform-specific.

reply
jitl
10 months ago
[-]
Browser web pages are quintessential long running programs! At least for Notion, a browser tab typically lives much longer (days to weeks) than our server processes (hours until next deploy). They're an event loop like a server often with multiple subprocesses, very much not a run-to-completion CLI tool. And errors do not terminate a web page.

The order of execution for unhandled errors is well-defined. The error unwinds up the call stack running catch and finally blocks, and if gets back to the event loop, then it's often dispatched by the system to an "uncaught exception" (sync context) or "unhandled rejection" (async context) handler function. In NodeJS, the default error handler exits the process, but you can substitute your own behavior which is common for long-running servers.

All that is to say, that yes, this does work since termination handler is called at the top of the stack, after the stack unwinds through the finally blocks.

reply
the_mitsuhiko
10 months ago
[-]
This is very useful for resource management of WASM types which might have different memory backing.
reply
jitl
10 months ago
[-]
Yeah, great for that use-case - memory management; it's great to get the DisposeStack that allows "moving" out of the current scope too, that's handy.

I adopted it for quickjs-emscripten (my quickjs in wasm thingy for untrusted code in the browser) but found that differing implementations between the TypeScript compiler and Babel lead to it not being reliably usable for my consumers. I ended up writing this code to try to work around the polyfill issues; my compiler will use Symbol.for('Symbol.dispose'), but other compilers may choose a different symbol...

https://github.com/justjake/quickjs-emscripten/blob/aa48b619...

reply
paulddraper
10 months ago
[-]
The `using` proposal is so bad. [1]

There is exactly zero reason to introduce a new variable binding for explicit resource management.

And now it doesn't support destructuring, etc.

It should have been

  using (const a = resource()) {
  }
Similar to for-of.

[1] https://github.com/tc39/proposal-explicit-resource-managemen...

[2] https://github.com/tc39/proposal-explicit-resource-managemen...

reply
pie_flavor
10 months ago
[-]
That is introducing a new variable binding, and strictly more verbose in all cases for no particular benefit. Destructuring was gone over many times; the problem with using it naively is that it's not clear upon first encountering it whether the destructured object or the fields destructured out of it are disposed. Destructuring is supported via DisposableStack in the proposal. This was already litigated to death and you can see the author's response in your links.
reply
paulddraper
10 months ago
[-]
> Destructuring was gone over many times; the problem with using it naively is that it's not clear upon first encountering it whether the destructured object or the fields destructured out of it are disposed.

Yes, and this trivially solves that!

    for (const { prop1, prop2 } of iterable)
                      ^               ^
               Destructuring       Iterable

    using (const { prop1, prop2 } of disposable)
                        ^               ^
               Destructuring       Disposable
No ambiguity. Very clear.

> That is introducing a new variable binding

No. const, let, var are variable bindings with rules about scope and mutability.

using adds to that list. And for the life of me I can't remember what it says about mutability.

using-of would keep that set.

> strictly more verbose in all cases for no particular benefit.

See above.

Additional benefit is that the lifetime of the object is more clear, and it's encouraged to be cleaned up more quickly. Rather than buried in a block with 50 lines before and 50 lines after.

> This was already litigated to death and you can see the author's response in your links.

Absolutely. The owners unfortunately decided to move forward with it.

Despite being awkward, subpar.

reply
MrJohz
10 months ago
[-]
Fwiw, I've been using `using` for the last year or so maybe, and I've found exactly one case where I've wanted to create a new explicit scope for the resource. In all other cases, having the resource live as long as the containing function/loop/whatever was the clearest option, and being forced to create a new scope would have made my code messier, more verbose, and more indented.

Especially as in the one case where it was useful to create an explicit scope, I could do that with regular blocks, something like

    console.log("before")
    {
      using resource = foo()
      console.log("during", resource) 
    }
    console.log("after")
Having used Python's `with` blocks a lot, I've found I much prefer Javascript's approach of not creating a separate scope and instead using the existing scoping mechanisms.
reply
ivan_gammel
10 months ago
[-]
Is it the same as try with resources in Java?

   try(var reader = getReader()) {
       // do read
   } // auto-close
reply
masklinn
10 months ago
[-]
It's similar, but more inspired by C#'s "using declaration", an evolution of the using blocks, which are the C# version of try-with-resource: `using` declarations don't introduce their own block / scope.

The original proposal references all of Python's context manager, Java's try-with-resource, and C#'s using statement and declaration: https://github.com/tc39/proposal-explicit-resource-managemen...

reply
OskarS
10 months ago
[-]
Only experience with C# using and Python context managers, but they always seemed like such an unsatisfying solution compared to C++/Rust style RAII. I mean, I get these are GC languages, but why can't just this happen at end of your regular scope? Why do you have to have these artificial new scopes?
reply
maxfurman
10 months ago
[-]
That's what's neat about the simpler C# version (and this proposal): it's bound to whatever block it's in and doesn't need its own scope/indentation
reply
neonsunset
10 months ago
[-]
using var stream = GetStream(); does not introduce new (lexical) scope, avoiding use means that the type system either has to understand move semantics (in case the stream is returned or move elsewhere, but then, what about sharing?) or have full-blown borrow checker which will emit drop.
reply
mattmanser
10 months ago
[-]
Yeah, as someone else has pointed out it's C# inspired, this is a C# example:

    public void AMethod() {
        //some code
        using var stream = thing.GetStream();
        //some other code
        var x = thing.ReadToEnd();
        //file will be automatically disposed as this is the last time file is used
        //some more code not using file
    } //any error means file will be disposed if initialized
You can still do the wrap if you need more fine grained control, or do anything else in the finally.

You can even nest them like this:

    using var conn = new SqlConnection(connString);
    using var cmd = new SqlCommand(cmd);
    conn.Open();
    cmd.ExecuteSql();

Edit: hadn't read the whole article, the javascript version is pretty good!
reply
creata
10 months ago
[-]
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.

reply
demurgos
10 months ago
[-]
What you describe is already the status quo today. This proposal is still a big improvement as it makes resource management less error prone when you're aware to use it and _standardizes the mechanism through the symbol_. This enables tooling to lint for the situations you're describing based on type information.
reply
0xfffafaCrash
10 months ago
[-]
Here’s some relevant discussion about some of the footguns:

https://github.com/typescript-eslint/typescript-eslint/issue...

https://github.com/tc39/proposal-explicit-resource-managemen...

I imagine there will eventually be lint rules for this somewhere and many of those using such a modern feature are likely to be using static analysis via eslint to help mitigate the risks here, but until it’s more established and understood and lint rules are fleshed out and widely adopted, there is risk here for sure.

https://github.com/typescript-eslint/typescript-eslint/issue...

To me it seems a bit like popular lint libraries just going ahead and adding the rule would make a big difference here

reply
akdor1154
10 months ago
[-]
There is pretty strong precedent for this design over in .NET land - if it was awful or notably inferior to `defer` I'm sure the Chrome engineering team would have taken notice.
reply
creata
10 months ago
[-]
C# has the advantage of being a typed language, which allows compilers and IDEs to warn in the circumstances I mentioned. JavaScript isn't a typed language, which limits the potential for such warnings.

Anyway, I didn't say it was "inferior to defer", I said that it seemed more error-prone than RAII in languages like Rust and C++.

Edit: Sorry if I'm horribly wrong (I don't use C#) but the relevant code analysis rules look like CA2000 and CA2213.

reply
masklinn
10 months ago
[-]
> Anyway, I didn't say it was "inferior to defer", I said that it seemed more error-prone than RAII in languages like Rust and C++.

It is, but RAII really isn't an option if you have an advanced GC, as it is lifetime-based and requires deterministic destruction of individual objects, and much of the performance of an advanced GC comes from not doing that.

Most GC'd language have some sort of finalizers (so does javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...) but those are unreliable and often have subtle footguns when used for cleanup.

reply
legulere
10 months ago
[-]
It’s still difficult to get right in cases where you hold a disposable as a member. Its not obvious if disposables passed in also get disposed and what’s right depends on the situation (think a string based TextWriter getting passed in a byte-based Stream) and you will need to handle double disposes.

Further C# has destructors that get used as a last resort effort on native resources like file descriptors.

reply
creata
10 months ago
[-]
> Further C# has destructors that get used as a last resort effort on native resources like file descriptors.

True, I was going to mention that, but I saw that JS also has "finalization registries", which seem to provide finalizer support in JS, so I figured it wasn't a fundamental difference.

reply
electroly
10 months ago
[-]
As a practical matter it's easy to forget in C# and it's up to you to remember. Those two analyzers are disabled by default and prone to both false positives and false negatives. They hardcoded the known behavior of a bunch of .NET classes to get it to be usable at all.
reply
akoboldfrying
10 months ago
[-]
Exactly this. No idea why you were downvoted.

The problem they are trying to solve is that the programmer could forget to wrap an object creation with try. But their solution is just kicking the can down the road, because now the programmer could forget to write "using"!

I was thinking that a much better solution would be to simply add a no-op default implementation of dispose(), and call it whenever any object hits end-of-scope with refcount=1, and drop the "using" keyword entirely, since that way programmers couldn't forget to write "using". But then I remembered that JavaScript doesn't have refcounts, and we can't assume that function calls to which the object has been passed have not kept references to it, expecting it to still exist in its undisposed state later.

OTOH, if there really is no "nice" solution to detecting this kind of "escape", it means that, under the new system, writing "using" must be dangerous -- it can lead to dispose() being called when some function call stored a reference to the object somewhere, expecting it to still exist in its undisposed state later.

reply
morsecodist
10 months ago
[-]
I feel it doesn't make sense to conflate resource management with garbage collection. The cleanup actions here are more like releasing a lock, deleting temporary files, or closing a connection. This doesn't lead to a lack of safety. These resources already need to deal with these uninitialised states. For example, consider a lock management object. You shouldn't assume you have the lock just because you have a reference to the manager resource. It's totally normal to have objects that require some sort of initialization.
reply
akoboldfrying
10 months ago
[-]
You seem to be making the argument that the new "using" doesn't create any new, dangerous behaviours. But I already agree with that -- I'm saying that it doesn't make anything better than it was before (a programmer who forgets to write "try" could equally forget to write "using", so the required level of programmer discipline is unchanged), plus, if you mistakenly persuade yourself that it's safe to write "using" everywhere all the time, you can introduce bugs like locks that are released too early.

Why might a programmer persuade themselves of that? Because otherwise "using" has no benefit at all, beyond a slightly sweeter syntax for wrapping the function body in "try ... catch (x) { for (o of objs) o.dispose(); }”.

> I feel it doesn't make sense to conflate resource management with garbage collection

Memory is just a resource, one that conveniently doesn't require anything to be done urgently when that its lifetime ends (unlike, say, locks). GC is a system for managing that resource -- or any resource with the same non-urgency property. For example, you can imagine a GC-based resource management system for a pool of DB connections.

> You shouldn't assume you have the lock just because you have a reference to the manager resource.

Some languages make it necessary to code in this way, where you need to always check if something is in a valid state before doing something with it, but that's unfortunate, because there are languages (like C++, which is horrible in so many other ways) where you can maintain the invariant that, if you have a reference to a lock, the associated resource is locked. The general idea -- Make Invalid States Unrepresentable -- is a fantastic way to improve code quality, so we should always be looking for ways to incorporate it into languages that don't yet have it. Parse Don't Validate is the same underlying idea.

reply
mistercow
10 months ago
[-]
Another point there is that JS has always gone to great lengths not to expose the GC in any way. For example, you can’t enumerate a WeakSet, because that would cause behavior to be GC dependent. Calling dispose when an object is collected would very explicitly cause the GC to have semantic effects, and I think that goes strongly against the JS philosophy.
reply
masklinn
10 months ago
[-]
FinalizationRegistry was added, like, 5 years ago.
reply
pwdisswordfishz
10 months ago
[-]
Yes, it and WeakRef are exceptions, but they are the only ones, designed to be deniable – if you delete globalThis.WeakRef; and delete globalThis.FinalizationRegistry; you go back to not exposing GC at all. WeakRef even has a special exception in the spec in that the .constructor property is optional, specifically so that handing a weak reference to some code does not necessarily enable it to create more weak references, so you can be also limited as to which objects' GC you can observe.

Though another problem is that the spec does not clearly specify when an object may be collected or allow the programmer to control GC in any way, which means relying on FinalizationRegistry may lead to leaks/failure to finalize unused resources (bad, but sometimes tolerable) or worse, use-after-free bugs (outright fatal) – see e.g. https://github.com/tc39/ecma262/issues/2650

reply
rafram
10 months ago
[-]
Finalizers aren’t destructors. The finalizer doesn’t get access to the object being GC’d, for one. But even more crucially, the spec allows the engine to call your finalizer anywhere between long after the object has been GC’d, and never.

They’re basically a nice convenience for noncritical resource cleanup. You can’t rely on them.

reply
masklinn
10 months ago
[-]
Yes? Congratulation you know what a finalizer is?

I was replying to this:

> would very explicitly cause the GC to have semantic effects, and I think that goes strongly against the JS philosophy.

Do you disagree that a finalizer provides for exactly that and thus can not be "strongly against the JS philosophy"?

reply
mistercow
10 months ago
[-]
I mean it’s an explicit violation of that philosophy as noted in the proposal:

> For this reason, the W3C TAG Design Principles recommend against creating APIs that expose garbage collection. It's best if WeakRef objects and FinalizationRegistry objects are used as a way to avoid excess memory usage, or as a backstop against certain bugs, rather than as a normal way to clean up external resources or observe what's allocated.

reply
mistercow
10 months ago
[-]
Fair, I wasn’t aware of that. But even so, there’s a big difference between a wonky feature intended for niche cases and documented almost entirely in terms of caveats, and “this is the new way to dispose of resources”.

And the point that this kind of thing is against the JS philosophy is pretty explicit:

https://w3ctag.github.io/design-principles/#js-gc

reply
russellbeattie
10 months ago
[-]
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.

reply
paavohtl
10 months ago
[-]
JS has used "well-known symbols"[1] to allow extending / overriding the functionality of objects for about 10 years. For example, an object is an iterable if it has a `[Symbol.iterator]` property. Symbols are valid object keys; they are not just string aliases.

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

reply
jitl
10 months ago
[-]
Symbols are a very safe way to introduce new "protocols" in either the language standard or for application code. This is because Symbol can never conflict with existing class definitions. If we use a string name for the method, then existing code semantics change.

Here are the well-known symbols that my NodeJS 22 offers when I `Symbol.<tab>`:

    Symbol.asyncDispose
    Symbol.asyncIterator
    Symbol.dispose
    Symbol.hasInstance
    Symbol.isConcatSpreadable
    Symbol.iterator
    Symbol.keyFor
    Symbol.length
    Symbol.match
    Symbol.matchAll
    Symbol.replace
    Symbol.search
    Symbol.species
    Symbol.split
    Symbol.toPrimitive
    Symbol.toStringTag
    Symbol.unscopables
reply
demarq
10 months ago
[-]
Yes you are missing something. You are not supposed to call these methods, they are for the runtime.

more specifically, javascript will call the [Symbol.dispose] when it detects you are exiting the scope of a "using" declaration.

reply
rafram
10 months ago
[-]
> identified not with a standard name (like Object.constructor() or Object.__proto__)

__proto__ was a terrible mistake. Google “prototype pollution”; there are too many examples to link. In a duck-typed language where the main mechanism for data deserialization is JSON.parse(), you can’t trust the value of any plain string key.

reply
uasi
10 months ago
[-]
> create a new global object named "Resource" which has the needed method prototypes that can be overwritten.

those methods could conflict with existing methods already used in other ways if you’d want to make an existing class a subclass of Resource.

reply
masklinn
10 months ago
[-]
> To make a "resource", you'll tack on a new magical method to your POJO, identified not with a standard name [...] nothing to do with Symbol's core purpose of creating unique identifiers.

The core purpose and original reason why Symbol was introduced in JS is the ability to create non-conflicting but well known / standard names, because the language had originally reserved no namespace for such and thus there was no way to know any name would be available (and not already monkey patched onto existing types, including native types).

> 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!

`Symbol.iterator` called by `for...of` is literally the original use case for symbols.

> 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.

Genuinely: what are you talking about.

reply
russellbeattie
10 months ago
[-]
> Genuinely: what are you talking about.

They added the get and set keywords to plain JS objects to identify getters and setters. So just add a dispose keyword. Like this:

    const obj = {
      log: ["a", "b", "c"],
      get first() {
        return this.log[0];
      },
      dispose cleanup() {
       // close resource
      } 
    };
Much cleaner.
reply
masklinn
10 months ago
[-]
Right, so instead of just grabbing a property by its well know unique name aka the entire point of symbols you need to first resolve the property descriptor, check a new completely unnecessary flag, and only then can you call the method. And of course that breaks if anyone either forgets the attribute or fails to check it when calling the parent method.

And you’re now wasting an entire keyword on a property with a fixed name, and code bases which already use that name with different semantics are unable to add `using` compatibility.

reply
Spivak
10 months ago
[-]
I hadn't considered how blessed I was to have __enter__ / __exit__ in Python for context managers and the more general Protocol concept that can be used for anything because lordy that definition looks ugly as sin. Even Perl looks on in horror of the sins JS has committed.
reply
russellbeattie
10 months ago
[-]
> Even Perl looks on in horror of the sins JS has committed.

LOL. What a great line.

It's getting pretty ridiculous, I agree. I don't understand the need for so many shorthands for one. All they do is make code illegible for the sake of saving a few keystrokes. Using the Symbol object that way is just ugly.

reply
bingemaker
10 months ago
[-]
Unsure if this is inspired from C++ RAII. RAII looks very elegant.

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

reply
masklinn
10 months ago
[-]
> Unsure if this is inspired from C++ RAII.

Not really. Both are ways to perform deterministic resource management, but RAII is a branch of deterministic resource management which most GC'd languages can not use as they don't have deterministic object lifetimes.

This is inspired by similar constructs in Java, C#, and Python (and in fact lifted from C# with some adaptation to JS's capabilities), and insofar as those were related to RAII, they were a step away from it, at least when it comes to Python: CPython historically did its resource management using destructors which would mostly be reliably and deterministically called on refcount falling to zero.

However,

1. this was an issue for non-refcounted alternative implementations of Python

2. this was an issue for the possibility of an eventual (if unlikely) move away from refcounting in CPython

3. destructors interact in awkward ways with reference cycles

4. even in a reference-counted language, destructors share common finaliser issues like object resurrection

Thus Python ended up introducing context managers as a means of deterministic resource management, and issuing guidance to avoid relying on refcounting and RAII style management.

reply
bingemaker
10 months ago
[-]
Thanks for the explanation
reply
senfiaj
10 months ago
[-]
I wrote an article with more examples https://waspdev.com/articles/2025-05-17/js-destructors-or-ex... . It's actually a simplified version of this https://github.com/tc39/proposal-explicit-resource-managemen... .
reply
morsecodist
10 months ago
[-]
I just wrote a blog post (https://morsecodist.io/blog/typescript-resource-management) about this feature. I love it and I feel it still hasn't caught on in the ecosystem.
reply
davidmurdoch
10 months ago
[-]
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.

reply
masklinn
10 months ago
[-]
The error was probably trying to write a generic `using`. In my experience languages which use higher order functions or macros for scope cleanup tend to build high-level utilities directly onto the lowest level features, it can be a bit repetitive but usually not too bad.

So in this case, rather than a generic `using` built on the even more generic `try/except` you should probably have built a `withFile` callback. It's a bit more repetitive, but because you know exactly what you're working with it's a lot less error prone, and you don't need to hope there's a ready made protocol.

It also provides the opportunity of upgrading the entire thing e.g. because `withFile` would be specialised for file interaction it would be able to wrap all file operations as promise-based methods instead of having to mix promises and legacy callbacks.

reply
davidmurdoch
10 months ago
[-]
Sure, the with* pattern is fine. I was just playing with the idea of C#s disposable pattern in JS.
reply
CreepGin
10 months ago
[-]
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

reply
taylorallred
10 months ago
[-]
Ngl, I was hoping “resources” was referring to memory.
reply
tempaccount420
10 months ago
[-]
Would be amazing to have a low-level borrow-checked subset of JS, as part of JS, so you can rewrite your hot loops in it.

Granted, you could also just import * from './low-level.wat' (or .c, and compile it automatically to WASM)

reply
sufianrhazi
10 months ago
[-]
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-...
reply
xmorse
10 months ago
[-]
Why didn't they go with `defer` which is the more ergonomic and established pattern?

`defer` also doesn't need to change the API of thousands of objects to use it, instead now you have to add a method any resource like object, or for things that are not objects, you can't even use this feature.

reply
masklinn
10 months ago
[-]
> Why didn't they go with `defer` which is the more ergonomic and established pattern?

Neither statement is true.

> `defer` also doesn't need to change the API of thousands of objects to use it

Callbacks can trivially be bridged to `using`:

    using _cb = {[Symbol.dispose]: yourCallbackHere};
There is also built-in support for cleanup callbacks via the proposal’s DisposableStack object.

> or for things that are not objects

This is javascript. Everything of note is an object. And again, callbacks can trivially be bridged to using

reply
bakkoting
10 months ago
[-]
For readability I'd recommend using DisposableStack instead of making an object, though either works:

    using disposer = new DisposableStack;
    disposer.defer(yourCallbackHere);
reply
sharlos201068
10 months ago
[-]
I can't think of any JS value that could need cleaning up that isn't an object.
reply
xmorse
10 months ago
[-]
what about functions that return a callback for disposal? like Redux store.subscribe, there are thousands more examples
reply
jmull
10 months ago
[-]
I would have preferred "defer", but "using" is a lot better than nothing.
reply
90s_dev
10 months ago
[-]
Using is more flexible, since it doesn't need a function call, but can simply assign a variable that implements [[dispose]]
reply
jmull
10 months ago
[-]
Only when you have an object that implements [Symbol.dispose]. If you don't, then you need to create one (like the wrapper in the example from the article) or bang out some boilerplate to explicitly make and use a DisposableStack().

So with using there's a little collection of language features to learn and use, and (probably more importantly), either app devs and library devs have to get on the same page with this at the same time, or app devs have to add a handful of boilerplate at each call site for wrappers or DisposableStacks.

reply
MrJohz
10 months ago
[-]
Fwiw, most backend frameworks and libraries where this sort of functionality makes sense are already using a polyfilled Symbol.dispose that will transparently work with the real thing if you start using it in an environment that supports it. I've been using this feature for a while via transpiled syntax, and it works pretty well.

On the frontend I suspect it'll take a bit longer to become ubiquitous, but I'm sure it'll happen soon enough.

reply
masklinn
10 months ago
[-]
They're basically dual of one another.

`using` is mostly more convenient, because it registers cleanup without needing extra calls, unlike `defer`.

And of course you can trivially bridge callbacks, either by wrapping a function in a disposeable literal or by using the DisposableStack/AsyncDisposableStack utility types which the proposal also adds.

reply
pier25
10 months ago
[-]
did this go through the TC39 or is this a V8 only feature?
reply
bri3d
10 months ago
[-]
reply
pier25
10 months ago
[-]
thanks!
reply
k__
10 months ago
[-]
Bun seems to have it and it's not using V8.
reply
smashah
10 months ago
[-]
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.
reply
hahn-kev
10 months ago
[-]
They're just adopting the same syntax that C# has used for a long time
reply
smashah
10 months ago
[-]
Ok, but it doesn't make it any less meaningless.

async, await, let, var, const, try, catch, yield are all meaningful and precise keywords

"use" "using" on the other hand is not a precise word at all. To any non c# person it could be used to replace any of the above words!

reply
demarq
10 months ago
[-]
So… drop
reply
lucasyvas
10 months ago
[-]
The more this stuff gets introduced the more I’m convinced to use Rust everywhere. I’m not saying this is the Rust way - it’s actually reminiscent of Python/C#. But, Rust does it better.

If we keep going down these roads, Rust actually becomes the simpler language as it was designed with all of these goals instead of shoe-horning them back in.

reply
CryZe
10 months ago
[-]
First it was "Why Do Animals Keep Evolving into Crabs?", now it's "Why Do Programming Languages Keep Evolving into Crabs?"
reply
demarq
10 months ago
[-]
Not sure I agree or maybe we do.

I think JavaScript should remain simple. If we really need this functionality we can bring in defer. But as a 1:1 with what is in golang. This in between of python and golang is too much for what JavaScript is supposed to be.

I definitely think that the web needs a second language with types, resource management and all sorts of structural guard rails. But continuing to hack into JavaScript is not it.

reply
mettamage
10 months ago
[-]
What do you mean by that? Is `drop` a language construct in another language?
reply
dminik
10 months ago
[-]
reply
brigandish
10 months ago
[-]
It's also in this comment, which reads like Gen Zed slang.

https://news.ycombinator.com/item?id=44012969

reply
brigandish
10 months ago
[-]
Drop?
reply
DemocracyFTW2
10 months ago
[-]
it's an annoying usage because you never know whether it means "sth new appeared" or "sth old stopped being available"
reply
john2x
10 months ago
[-]
New drop just dropped
reply
pacifika
10 months ago
[-]
Drop and biweekly
reply
roschdal
10 months ago
[-]
JavaScript new features: segmentation faults, memory leaks, memory corruption and core dumps.
reply
cluckindan
10 months ago
[-]
Nah, it still doesn’t let you allocate or free memory manually.
reply
sylware
10 months ago
[-]
Still implemented with the super villain language, c++?
reply
master-lincoln
10 months ago
[-]
It depends on what language the Javascript engine is implemented in. For v8 that's c++ yeah. I would agree with Google being a super villain nowadays, but others use c++ too so I would think it's unfair to call it supervillain language...
reply
sylware
10 months ago
[-]
c++ is an abomination, come on, let's stay honest, we all know that by now. It should not be even taught anymore.

It IS the mother of all super villain computer languages.

We have to stop to be hypocrit now.

reply
bvrmn
10 months ago
[-]
Context managers: exist.

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

It's hilariously awkward.

reply
masklinn
10 months ago
[-]
> JS: drop but we couldn't occupy a possibly taken name, Symbol for the win!

You're about a decade late to the party?

That is the entire point of symbols and "well known symbols", and why they were introduced back in ES6.

reply
bvrmn
10 months ago
[-]
And I didn't use it because there was no need.

Resource scoping is important feature. Context managers (in python) are literally bread and butter for everyday tasks.

It's awkward not because of Symbol, it introduces new syntax tied to existing implicit scopes. It's kinda fragile based on Go experience. Explicit scoping is a way more predictable.

reply
demarq
10 months ago
[-]
nah, Symbol has been traits for javascript for quite a while eg. Symbol.iterator

It's the "dispose" part where the new name is decided.

reply
rounce
10 months ago
[-]
Traits in the way that a roller-skate is a car.
reply
90s_dev
10 months ago
[-]
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 :(
reply
neRok
10 months ago
[-]
I'm just a hobbyist and have some scriplets I've written to "improve" things on various websites. As part of that I needed an uninstall/undo feature - so with my "bush league" code I would do `window.mything = {something}`, and based upon previous dabblings with the likes of python/c#/go, I presumed I would be able to do `delete window.mything` and it would auto-magically call a function I would have written to do the work. So the new `[Symbol.dispose]()` feature/function would have done what I was looking for - but really, it;s not a big deal, because all that I actually had to do was write an interface spec'ing `{}.remove()` method, and call it where needed.

(This paragraph is getting off topic, but still... ) Below is my exact interface that I have in a .d.ts file. The reason for that file is because I like typed languages (ie TypeScript), but I don't want to install stuff like node-js for such simple things. So I realised vscode can/will check js files as ts on-the-go, so in a few spots (like this) I needed to "type" something - and then I found some posts about svelte source code using js-docs to type their code-base instead of typescript. So that's basically what I've done here...

  export global {
    interface Window {
        MyThing?: {remove: ()=>any}
    }
  }
So chances are that in the places you could use this feature, you've probably already got an "interface" for closing things when done (even if you haven't defined the interface in a type system).
reply
baq
10 months ago
[-]
If you’re using the withResource() pattern, you’re already effectively doing this, so yeah. If you’re using try/finally, it might be worth taking a second look.
reply
morsecodist
10 months ago
[-]
It doesn't seem to be widely used yet. I used it to clean up temporary files for a loader that downloads them.
reply
spelley
10 months ago
[-]
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.

reply
masklinn
10 months ago
[-]
> This looks most similar to golang’s defer. It runs cleanup code when leaving the current scope.

It's nothing like go's defer: Go's defer is function-scoped and registers a callback, using is block-scoped and registers an object with a well defined protocol.

> It differs from [...] c# “using,”

It's pretty much a direct copy of C#'s `using` declaration (as opposed to the using statement): https://learn.microsoft.com/en-us/dotnet/csharp/language-ref....

This can also be seen from the proposal itself (https://github.com/tc39/proposal-explicit-resource-managemen...) which cites C#'s using statement and declaration, Java's try-with-resource, and Python's context managers as prior art, but only mentions Go's defer as something you can emulate via DisposableStack and AsyncDisposableStack (types which are specifically inspired by Python's ExitStack),

reply