10 ms·
JavaScript's New Superpower: Explicit Resource Management
- qprofyeh 1y agoCan 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?
- Garlef 1y agoBecause this approach also works for stuff that is not a class instance.
- matharmin 1y agoDestructors 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.
- senfiaj 1y agoFor 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-js-developer-must-know-in-2025-part-2#weakref https://waspdev.com/articles/2025-04-09/features-that-every-... https://waspdev.com/articles/2025-04-09/features-that-every-js-developer-must-know-in-2025-part-2#finalizationregistry 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.
- feverzsj 1y agoBecause javascript is uncivilized.
- masklinn 1y agoDestructors 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 1y agoWhat does "HoF" stand for?
- masklinn 1y agohigher 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 }
- deleted 1y ago[deleted]
- pwdisswordfishz 1y agoThere is no such thing as an anonymous property in JavaScript. Your question doesn't make sense. What else could this possibly be?
- CreepGin 1y agoNeed 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 https://github.com/Singtaa/OneJS
- smashah 1y agoI 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 1y agoThey're just adopting the same syntax that C# has used for a long time
- smashah 1y agoOk, 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!
- deleted 1y ago[deleted]
- demarq 1y agoSo… drop
- mettamage 1y agoWhat do you mean by that? Is `drop` a language construct in another language?
- brigandish 1y agoIt's also in this comment, which reads like Gen Zed slang. https://news.ycombinator.com/item?id=44012969 https://news.ycombinator.com/item?id=44012969
- dminik 1y agoYes, it's called Drop in rust: https://doc.rust-lang.org/std/ops/trait.Drop.html https://doc.rust-lang.org/std/ops/trait.Drop.html
- brigandish 1y agoDrop?
- DemocracyFTW2 1y agoit's an annoying usage because you never know whether it means "sth new appeared" or "sth old stopped being available"
- lucasyvas 1y agoThe 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.
- bvrmn 1y agoContext managers: exist. JS: drop but we couldn't occupy a possibly taken name, Symbol for the win! It's hilariously awkward.
- demarq 1y agonah, Symbol has been traits for javascript for quite a while eg. Symbol.iterator It's the "dispose" part where the new name is decided.
- rounce 1y agoTraits in the way that a roller-skate is a car.
- masklinn 1y 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.
- bvrmn 1y agoAnd 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.
- deleted 1y ago[deleted]
- akokanka 1y ago[dead]
- havkom 1y agoReminds 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).
- masklinn 1y agoIt'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.
- pwdisswordfishz 1y agoThat's because the author of the proposal is from Microsoft and has repeatedly shot down counter-suggestions that made the syntax look different from C#. https://github.com/tc39/proposal-explicit-resource-management/issues/237 https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-management/issues/199 https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-management/issues/92 https://github.com/tc39/proposal-explicit-resource-managemen... https://github.com/tc39/proposal-explicit-resource-management/issues/77#issuecomment-1030679166 https://github.com/tc39/proposal-explicit-resource-managemen...
- russellbeattie 1y agoMaybe 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.
- paavohtl 1y agoJS 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/Reference/Global_Objects/Symbol#well-known_symbols https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- demarq 1y agoYes 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.
- sufianrhazi 1y agoThis 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-great.html https://abstract.properties/explicit-resource-management-is-...
- creata 1y agoThis 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.
- akdor1154 1y agoThere 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 1y agoC# 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 1y 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/Reference/Global_Objects/FinalizationRegistry https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...) but those are unreliable and often have subtle footguns when used for cleanup.
- legulere 1y agoIt’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.
- roschdal 1y agoJavaScript new features: segmentation faults, memory leaks, memory corruption and core dumps.
- cluckindan 1y agoNah, it still doesn’t let you allocate or free memory manually.
- TekMol 1y agoTheir 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 1y agoThe 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.
- jitl 1y agoBrowser 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.
- sylware 1y agoStill implemented with the super villain language, c++?
- master-lincoln 1y agoIt 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...
- sylware 1y agoc++ 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.
- mineogames 1y ago[dead]
- the_mitsuhiko 1y agoThis is very useful for resource management of WASM types which might have different memory backing.
- jitl 1y agoYeah, 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/aa48b619983f02c5691d989c0771a3421178ce4b/packages/quickjs-emscripten-core/src/lifetime.ts#L52-L60 https://github.com/justjake/quickjs-emscripten/blob/aa48b619...
- deleted 1y ago[deleted]
- ivan_gammel 1y agoIs it the same as try with resources in Java? try(var reader = getReader()) { // do read } // auto-close
- masklinn 1y agoIt'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-management?tab=readme-ov-file#prior-art https://github.com/tc39/proposal-explicit-resource-managemen...
- OskarS 1y agoOnly 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?
- neonsunset 1y agousing 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.
- maxfurman 1y agoThat'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
- mattmanser 1y agoYeah, 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!
- 0xCE0 1y agoI 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.")); } });
- pwdisswordfishz 1y agoIndenting helps.
- kubb 1y agoAlso practice, programming is hard, but just because one person doesn't understand something, doesn't mean it's impossible or a bad idea.
- xg15 1y agoAlso, 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.
- davidmurdoch 1y agoI tried to write a `using` utility for JS a few years ago: https://gist.github.com/davidmurdoch/dc37781b0200a2892577363f30b6c545 https://gist.github.com/davidmurdoch/dc37781b0200a2892577363... It's not very ergonomic so I never tried to use it anywhere.
- masklinn 1y agoThe 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 1y agoSure, the with* pattern is fine. I was just playing with the idea of C#s disposable pattern in JS.
- bingemaker 1y agoUnsure if this is inspired from C++ RAII. RAII looks very elegant. `[Symbol.dispose]()` threw me off
- masklinn 1y 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.
- bingemaker 1y agoThanks for the explanation
- xg15 1y agoThis 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.
- berkes 1y agoIsn't this typically solved with polyfills in the JavaScript world?
- mst 1y agoI 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.
- mst 1y agoIf you want to play with this, Bun 1.0.23+ seems to already have support: https://github.com/oven-sh/bun/discussions/4325 https://github.com/oven-sh/bun/discussions/4325
- spelley 1y agoThis 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.
- masklinn 1y 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-reference/proposals/csharp-8.0/using#using-declaration 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-management 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),
- jmull 1y agoI would have preferred "defer", but "using" is a lot better than nothing.
- 90s_dev 1y agoUsing is more flexible, since it doesn't need a function call, but can simply assign a variable that implements [[dispose]]
- masklinn 1y agoThey'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.
- jmull 1y agoOnly 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.
- MrJohz 1y agoFwiw, 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.
- mst 1y agoOh 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.
- masklinn 1y agoNote 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.
- MrResearcher 1y agoWhat 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?
- bakkoting 1y agoYup, 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.
- 90s_dev 1y agoWhen 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 :(
- baq 1y agoIf 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.
- neRok 1y agoI'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).
- morsecodist 1y agoIt doesn't seem to be widely used yet. I used it to clean up temporary files for a loader that downloads them.
- taylorallred 1y agoNgl, I was hoping “resources” was referring to memory.
- tempaccount420 1y agoWould 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)
- qudat 1y agoResource 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 https://bower.sh/why-structured-concurrency Library that leverages structured concurrency: https://frontside.com/effection https://frontside.com/effection
- vaylian 1y agoI 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.
- Kalabasa 1y agoObject 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]()
- TheRealPomax 1y agopretty sure they were asking about the dynamic property name, { [thing]: ... }
- homebrewer 1y agohttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names 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.
- deleted 1y ago[deleted]
- MrJohz 1y agoDynamic 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.
- pier25 1y agodid this go through the TC39 or is this a V8 only feature?
- morsecodist 1y agoI just wrote a blog post (https://morsecodist.io/blog/typescript-resource-management 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.
- nayuki 1y agoThis proposal reeks of "What color is your function?" https://journal.stuffwithstuff.com/2015/02/01/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.
- jupp0r 1y agoI 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.
- swyx 1y agolook 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
- notnullorvoid 1y agoCompelling? 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.
- throw10920 1y agoThis 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.
- senfiaj 1y agoI wrote an article with more examples https://waspdev.com/articles/2025-05-17/js-destructors-or-explicit-resource-management 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-management https://github.com/tc39/proposal-explicit-resource-managemen... .
- paulddraper 1y agoThe `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-management/issues/15 https://github.com/tc39/proposal-explicit-resource-managemen... [2] https://github.com/tc39/proposal-explicit-resource-management/issues/76 https://github.com/tc39/proposal-explicit-resource-managemen...
- pie_flavor 1y agoThat 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.
- paulddraper 1y 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.
- xmorse 1y agoWhy 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.
- masklinn 1y 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
- sharlos201068 1y agoI can't think of any JS value that could need cleaning up that isn't an object.
- xmorse 1y agowhat about functions that return a callback for disposal? like Redux store.subscribe, there are thousands more examples