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