javascriptraii

Javascript release resources automatically (like RAII)


My general question is what techniques can I use to ensure that resources are cleaned up/released in Javascript? Currently, I am taking the C (without goto) approach of finding every execution path to a return or exception in my functions and ensuring clean up occurs.

My specific example is this: In Node.js I am using mutexes (through file locks) in object member functions (I need the mutual exclusion, because I run multiple instances of the Node.js application and have race conditions when different instances interact with the file system).

For example, in C++ I would do something like the following:

void MyClass::dangerous(void) {
     MyLock lock(&this->mutex);
     ...
     // at the end of this function, lock will be destructed and release this->mutex.
}

As far as I can tell, JavaScript doesn't provide any RAII functionality. In C, I would use gotos to unwind my resource allocation in the event of an error so that I only have one return path from a function.

What are some techniques to achieve a similar effect in Javascript?


Solution

  • 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 {
            await func(resource);
        } catch(e) {
            throw e;
        } finally {
            await resource.destructor();
        }
    }
    
    async function main() {
        console.log("Starting program");
        const fpath = "somewhere.txt";
        await run_with(new MockFileIO(fpath), async (f) => {
            await f.write("hello");
            await 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.