xg15 an hour 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.

  • spion an hour 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.

  • berkes an hour ago

    Isn't this typically solved with polyfills in the JavaScript world?

    • mst 4 minutes 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.

havkom 4 hours 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).

TekMol 3 hours 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?

  • teraflop 3 hours 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.

0xCE0 2 hours 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.")); } });

  • snickerbockers an hour 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.

    • eastbound 15 minutes ago

      Oh, we must upgrade, because of vulnerabilities. All the vulnerabilities found in 90% of this moot code.

      Ok, point taken.

    • exe34 27 minutes ago

      You wrote out loud what I've been thinking quietly.

      • gchamonlive 25 minutes ago

        From the lack of punctuation I think you can also rap it out loud.

    • the_arun 25 minutes ago

      Your paragraph is as complicated as the code we create over time. Is this your point? Then I take it.

  • notpushkin 6 minutes 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 :-)
  • gavinray an hour 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.

  • pwdisswordfishz an hour ago

    Indenting helps.

    • xg15 an hour 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.

    • kubb an hour 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.

      • lukan 26 minutes 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.

      • exe34 26 minutes ago

        It's also great for job security if very few people would be able to work on it.

  • 77pt77 21 minutes ago

    LLMs will do only this and you'll love it.

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

  • stephenr 11 minutes 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.

qprofyeh 5 hours 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?

  • masklinn 3 hours 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

    • nh2 2 hours ago

      What does "HoF" stand for?

      • masklinn 2 hours 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
          }
  • senfiaj 4 hours 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.

  • matharmin 4 hours 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.

  • Garlef 5 hours ago

    Because this approach also works for stuff that is not a class instance.

  • pwdisswordfishz 2 hours ago

    There is no such thing as an anonymous property in JavaScript. Your question doesn't make sense. What else could this possibly be?

  • feverzsj 3 hours ago

    Because javascript is uncivilized.

bingemaker an hour ago

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

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

davidmurdoch 2 hours 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.

  • masklinn an hour 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.

    • davidmurdoch an hour ago

      Sure, the with* pattern is fine. I was just playing with the idea of C#s disposable pattern in JS.

ivan_gammel 3 hours ago

Is it the same as try with resources in Java?

   try(var reader = getReader()) {
       // do read
   } // auto-close
  • masklinn 3 hours 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...

  • mattmanser 3 hours 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!
creata 4 hours 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.

  • demurgos 3 hours 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.

  • 0xfffafaCrash 2 hours 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

  • akdor1154 3 hours 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.

    • creata 3 hours 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.

      • masklinn 3 hours 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.

      • legulere 2 hours 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.

        • creata 2 hours 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.

  • akoboldfrying 3 hours 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.

    • mistercow 3 hours 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.

      • masklinn 3 hours ago

        FinalizationRegistry was added, like, 5 years ago.

        • pwdisswordfishz 2 hours 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

        • rafram 2 hours 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.

          • masklinn 2 hours 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"?

            • mistercow 2 hours 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.

        • mistercow 2 hours 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

the_mitsuhiko 3 hours ago

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

CreepGin 5 hours 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

bvrmn 4 hours ago

Context managers: exist.

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

It's hilariously awkward.

  • masklinn 3 hours 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.

  • demarq 4 hours 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.

roschdal 3 hours ago

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

russellbeattie 4 hours 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.

  • demarq 4 hours 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.

  • rafram 2 hours 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.

  • uasi 4 hours 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.

  • masklinn 2 hours 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.

  • Spivak 3 hours 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.

demarq 5 hours ago

So… drop

smashah 5 hours 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.

  • hahn-kev 4 hours ago

    They're just adopting the same syntax that C# has used for a long time

sylware 3 hours ago

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

  • master-lincoln 3 hours 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...