JavaScript's New Superpower: Explicit Resource Management
249 comments
·May 17, 2025nayuki
jupp0r
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.
swyx
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
notnullorvoid
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.
throw10920
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.
paulddraper
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.
bla3
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.
anon291
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)
rtpg
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!
dingi
Indeed. Virtual threads, structured concurrency and scoped values are great features.
timewizard
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.
rtpg
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!
timewizard
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.
vips7L
Beyond happy Java made that decision as well.
mst
Oh my word.
const defer = f => ({ [Symbol.dispose]: f })
using defer(() => cleanup())
That only just occurred to me. To everybody else who finds it completely obvious, "well done" but it seemed worthy of mention nonetheless.masklinn
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.MrResearcher
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?
bakkoting
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.masklinn
> 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.MrJohz
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.
mattlondon
Just like golang. Nice.
xg15
This is a great idea, but:
> Integration of [Symbol.dispose] and [Symbol.asyncDispose] in web APIs like streams may happen in the future, so developers do not have to write the manual wrapper object.
So for the foreseeable future, you have a situation where some APIs and libraries support the feature, but others - the majority - don't.
So you can either write your code as a complicated mix of "using" directives and try/catch blocks - or you can just ignore the feature and use try/catch for everything, which will result in code that is far easier to understand.
I fear this feature has a high risk of getting a "not practically usable" reputation (because right now that's what it is) which will be difficult to undo even when the feature eventually has enough support to be usable.
Which would be a real shame, as it does solve a real problem and the design itself looks well thought out.
jitl
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"
MrJohz
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.
bakkoting
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.berkes
Isn't this typically solved with polyfills in the JavaScript world?
mst
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.
sroussey
Much nicer than just adding your symbol method to the original class. :p
berkes
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.
spion
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.
jitl
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"?
spion
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.
someothherguyy
Isn't disconnecting a resize observer a poor example of this feature?
TheRealPomax
> 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.
havkom
Reminds me of C#.. IDisposible and IAsyncDisposible in C# helps a lot to write good mechanisms for things that should actually be abstracted in a nice way (such as locks handling, queue mechanisms, temporary scopes for impersonation, etc).
pwdisswordfishz
That'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-managemen...
https://github.com/tc39/proposal-explicit-resource-managemen...
https://github.com/tc39/proposal-explicit-resource-managemen...
https://github.com/tc39/proposal-explicit-resource-managemen...
spankalee
That looks like a lot of very reasonable responses to me.
Zacru
And he was just laid off. https://news.ycombinator.com/item?id=43978589
null
null
masklinn
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.
vaylian
I understand that JavaScript needs to maintain backwards compatibility, but the syntax
[Symbol.dispose]()
is very weird in my eyes. This looks like an array which is called like a function and the array contains a method-handle.
What is this syntax called? I would like to learn more about it.
zdragnar
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.
a4isms
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...
mceachen
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.
whizzter
This is the most important reason!
MrJohz
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.
cluckindan
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"
MrJohz
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.
homebrewer
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.ff2400t
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.
null
90s_dev
const o = {}
o["foo"] = function(){}
o["foo"]()
let key = "foo"
o[key]()
key = Symbol.dispose ?? Symbol.for('dispose')
o[key]()
o[Symbol.dispose]()
paulddraper
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.
vaylian
> 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?
masklinn
[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
qudat
Resource management, especially when lexical scoping is a feature, is why some of us have been working on bringing structured concurrency to JS: https://bower.sh/why-structured-concurrency
Library that leverages structured concurrency: https://frontside.com/effection
0xCE0
I don't understand how somebody can code like this and reason/control anything about the program execution :)
async (() => (e) { try { await doSomething(); while (!done) { ({ done, value } = await reader.read()); } promise .then(goodA, badA) .then(goodB, badB) .catch((err) => { console.error(err); } catch { } finally { using stack = new DisposableStack(); stack.defer(() => console.log("done.")); } });
snickerbockers
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.
lioeters
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!?
exe34
You wrote out loud what I've been thinking quietly.
gchamonlive
From the lack of punctuation I think you can also rap it out loud.
eastbound
Oh, we must upgrade, because of vulnerabilities. All the vulnerabilities found in 90% of this moot code.
Ok, point taken.
the_arun
Your paragraph is as complicated as the code we create over time. Is this your point? Then I take it.
90s_dev
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.TheRealPomax
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.postalrat
[flagged]
soulofmischief
That is a matter of opinion. JavaScript allows you to use either convention at your preference. Personally, I feel my code looks much, much cleaner without semicolons. I also use whitespace liberally.
90s_dev
For the longest time, I used them just in case it would otherwise cause a bug. But TypeScript fully takes this into account and checks for all these scenarios.
gavinray
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.
johnisgood
And Lisp, and Forth... :D
90s_dev
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.
notpushkin
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 :-)pwdisswordfishz
Indenting helps.
xg15
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.
wpollock
There used to be a great book for this, "JavaScript The Good Parts". Is there a well-respected equivalent for JavaScript in 2025?
kubb
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
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
It's also great for job security if very few people would be able to work on it.
jitl
you can write horrid code intentionally in any programming language
mrweasel
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.
neonsunset
> parts of the more modern C# is also a confusing mess
Do you have any examples?
stephenr
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.
chrisweekly
The overwhelming majority of serious work in JS is authored in TypeScript.
stephenr
That just Sounds like an even stronger argument to add types to the language.
cluckindan
That is a ”no true Scotsman” argument.
cluckindan
Someone needs to start creating leftPad and isOdd type troll packages in Rust just so we can ridicule the hubris.
maleldil
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.
paulddraper
> and yet is still missing basic things like parameter types
Like Bash, Python, Ruby?
stephenr
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.
TheRealPomax
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.
mst
If you want to play with this, Bun 1.0.23+ seems to already have support: https://github.com/oven-sh/bun/discussions/4325
qprofyeh
Can someone explain why they didn’t go with (anonymous) class destructors? Or something other than a Symbol as special object key. Especially when there are two Symbols (different one for asynchronous) which makes it a leaky abstraction, no?
masklinn
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
What does "HoF" stand for?
masklinn
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
}
matharmin
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.
senfiaj
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.
Garlef
Because this approach also works for stuff that is not a class instance.
pwdisswordfishz
There is no such thing as an anonymous property in JavaScript. Your question doesn't make sense. What else could this possibly be?
feverzsj
Because javascript is uncivilized.
TekMol
Their first example is about having to have a try/finally block in a function like this:
function processData(response) {
const reader = response.body.getReader();
try {
reader.read()
} finally {
reader.releaseLock();
}
}
So that the read lock is lifted even if reader.read() throws an error.Does this only hold for long running processes? In a browser environment or in a cli script that terminates when an error is thrown, would the lock be lifted when the process exits?
teraflop
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.
jitl
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.
the_mitsuhiko
This is very useful for resource management of WASM types which might have different memory backing.
jitl
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...
paulddraper
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...
pie_flavor
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.
paulddraper
> 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.
MrJohz
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.
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.