As others might have noted, you'll want to use try/finally. Creating a wrapper function to simulate the lifetime scope will likely be more comfortable coming from c++. Try running the following code in the javascript console to get an example of its usage:
C++ Style
class MockFileIO {
constructor(path) {
console.log("Opening file stream to path", path);
this.path = path;
}
destructor() {
console.log("Closing file stream to path", this.path);
}
write(str) {
console.log("Writing to file: ", str);
}
}
async function run_with(resource, func) {
try {
func(resource);
} catch(e) {
throw e;
} finally {
resource.destructor();
}
}
async function main() {
console.log("Starting program");
const fpath = "somewhere.txt";
await run_with(new MockFileIO(fpath), (f) => {
f.write("hello");
f.write("world");
});
console.log("returning from main");
}
main();
Golang style
I have since found a paradigm that plays better with the way I personally use javascript. It's based on golang's defer statement. You simply wrap your code in a "scope" IIFE, and when that function is left for any reason, the deferred expressions are executed in reverse order, awaiting any promises.
Usage:
scope(async (defer) => {
const s = await openStream();
defer(() => closeStream(s));
const db = new DBConnection();
defer(() => db.close());
throw new Error("oh snap"); // could also be return
// db.close() then closeStream(s)
});
Scopes can return values and are async. Here's an example of the same function written without, and then with the defer technique:
// without defer
async function getUser() {
const conn = new DB();
const user = await conn.getUser();
conn.close();
return user;
}
// this is bad! conn.getUser could throw an error.
becomes:
// with defer
async function getUser() {
return await scope(async defer => {
const conn = new DB();
defer(() => conn.close());
return await conn.getUser();
});
}
// conn.close is always called, even after error.
That's basically it. Scopes can also be nested. The code to define scope is quite small:
async function scope(fn) {
const stack = [];
const defer = (action) => {
stack.push(action);
};
const errs = [];
try {
return await fn(defer);
} catch(e) {
errs.push(e);
} finally {
while (stack.length) {
try {
await (stack.pop())();
} catch(e) {
errs.push(e);
}
}
for (const e of errs.slice(1)) {
await error("error in deferred action: " + e);
}
if (errs.length) {
throw errs[0]; // eslint-disable-line
}
}
}
scope immediately executes the callback and collects all deferred functions into a stack. When the function exits (either by return or error), the defer stack is popped until all the deferreds have been evaluated. Any errors that happen in deferred functions themselves are collected into an error list, and the first one is thrown when the "scope" exits. I've used this technique (this very code, actually) in a very critical, low-error-tolerance daemon I wrote for work, and it has stood the test of time. I hope this helps anyone who is running into this situation.