Node domains as a replacement for try catch

If you are reading this article I will assume you are already familiar with the difficulties in reliably handling exceptions in node and that you are searching for a better way.

There are a few brave souls out there trying to use domains as the main way of handling exceptions. Since I am in the midst of building a fairly large project using domains I thought it would be useful to post some information on my experience in this endeavour and any gotchas that I have experienced.

If you don't know much about domains, you might want to read my recent post on How Domains Work.

So, can you use them as a replacement for try catch exception handling? Sadly the answer is no, try catch and nodejs domains both behave differently and you will probably need a mix of both to achieve your goals. That said, I am using domains as my primary exception handling mechanism with try catch used sparingly.

So, let's start with how I use domains. I don't bother with them for synchronous functions to start with, they don't really offer me much value but when it comes to handling exceptions in asynchronous functions there is a big benefit. Consider the following well behaved asynchronous function using try catch.

function getAddressTryCatch(callback){  
    try{  
        // A trivial example of some synchronous work that might throw an exception  
        var domainName = "nodejs" + ".org";  

        dns.lookup(domainName, 4, function(err, address){  
            if(err){  
                return callback(err);  
            }  
            try{  
                //This is some other code that might throw an exception  
                console.log(address);  
                return callback(null, address);  
            }  
            catch(err){  
                return callback(err);  
            }  
        })  
    }  
    catch(err){  
        // Returning callback on process.nextTick to keep this function fully async, see: http://nodejs.org/api/process.html#process_process_nexttick_callback  
        process.nextTick(function(){callback(err)});  
        return;  
    }  

}  

No matter where an exception occurs when using this function it will be returned to the callback function instead of being thrown. We are even guarding against synchronous errors being thrown from dns.lookup(). While it is unlikely that one of the core functions in nodejs would throw an Error rather than returning it in a callback, my experience with some libraries is that you need to guard against this. Now look at the equivalent function using domains.

function getAddressDomain(callback){  
    var d = domain.create().on("error", function(err) {  
        //We don't need to worry about process.nextTick due to the way domains work  
        return callback(err);  
    });  
    // A trivial example of some synchronous work that might throw an exception  
    d.enter();  
    var domainName = "nodejs" + ".org";  

    dns.lookup(domainName, 4, d.intercept(function(address){  
            //This is some other code that might throw an exception  
            console.log(address);  
            return callback(null, address);  
    }));  

    d.exit();  

}  

Certainly less code to write and still does the same job. So, are these two examples really equivalent? Well yes and no. You can place an exception in either the synchronous part of the function or the callback and you will get the same output. The big difference between these approaches is with try catch, you have the option of continuing execution after an exception occurs but with domains your function exits and you have no way of resuming execution. Consider the following simple example:

function checkFileStats(pathArray, callback){  
    try{  
        // This will throw in v0.10 if one of the elements in the array is not a string  
        var filePath = path.join(pathArray);  
    }  
    catch(err){  
        if(err instanceof TypeError){  
            // contrived example default back to path of this file  
            console.log("changing filepath to default")  
            filepath = require.main.filename;  
        }else{  
            process.nextTick(function(){callback(err)});  
            return;  
        }  

    }  
    //Go on to do something async here - more contrived examples, even if we hit a TypeError above , this will still execute  
    process.nextTick(function(){  

        console.log("Did something Async");  
        return callback();  
    })  
}  

We are able to catch the error, handle it and move on. With domains we aren't given that luxury, we have to exit the function we are in. A similar function in node would look like this.

function checkFileStatsDomain(pathArray, callback){  
    var d = domain.create().on("error", function(err) {  
        if(err instanceof TypeError){  
            // contrived example default back to path of this file  
            console.log("changing filepath to default")  
            filepath = require.main.filename  
        }else{  
            return callback(err);  

        }  
    });  
    d.enter();  
    // This will throw in v0.10 if one of the elements in the array is not a string  
    var filePath = path.join(pathArray);  

    //GO on to do something async here  
    process.nextTick(function(){  
        console.log("We don't get here if we encounter an error earlier");  
        return callback();  
    });  
    d.exit();  

}  

So if we call the domain version of the function with an invalid pathArray, we never get to execute line 17 and start the async function. Execution is not resumed. Of course there are hacks to get around this, but there is no need to use them because try catch and domains can coexist. If you rewrite the function as:

function checkFileStatsDomainTryCatch(pathArray, callback){  
    var d = domain.create().on("error", function(err) {  
        return callback(err);  
    });  
    d.enter();  
    // This will throw in v0.10 if one of the elements in the array is not a string  
    try{  
        var filePath = path.join(pathArray);  
    }  
    catch(err){  
        if(err instanceof TypeError){  
            // contrived example default back to path of this file  
            console.log("changing filepath to default")  
            filepath = require.main.filename  
        }else{  
            throw err;  
        }  

    }  

    //Go on to do something async here  
    process.nextTick(function(){  
        console.log("We now make it here if we have a TypeError");  
        return callback();  
    });  
    d.exit();  

}  

It is able to resume after the TypeError and also reap the benefits of domains for the rest of the function.

There is however a scenario where try catch can interfere with the operation of domains and that is demonstrated below. By now you would expect what the following code should do.

var domain = require("domain");  
var d = domain.create().on("error";, function(err) {  
    console.log("This Error Will be Caught");  
});  

d.enter();  
throw new Error("foo");  
d.exit();  

But what is a little unexpected is what might happen if you write the following code.

var domain = require("domain");  
var d = domain.create().on("error";, function(err) {  
    console.log("The Domain handler never get's called");  
});  

try{  
    d.enter();  
    throw new Error("foo");  
    d.exit();  
}catch(err){  
    console.log("The Error is only caught here");  
}  

Strange huh? The reason this is happening is because of the way node domains work . They rely on what is effectively the process.on("uncaughtException") event firing. Because we have wrapped our code in a try catch block, any exception that is thrown will be caught and never bubble up to the uncaughtException handler.

This has implications for our original async function. Repeated here for simplicity.

function getAddressDomain(callback){  
    var d = domain.create().on("error", function(err) {  
        //We don't need to worry about process.nextTick due to the way domains work  
        return callback(err);  
    });  
    // A trivial example of some synchronous work that might throw an exception  
    d.enter();  
    var domainName = "nodejs" + ".org";  

    dns.lookup(domainName, 4, d.intercept(function(address){  
            //This is some other code that might throw an exception  
            console.log(address);  
            return callback(null, address);  
    }));  

    d.exit();  

}  

If this function is called inside a try catch block and an exception occurs in the function, it is at risk of being caught by the try catch handler and short circuiting the domain.on('error') handler. I say at risk because only synchronous code will get caught by the try catch block. If an error occurs in the asynchronous block it will be caught by the domain. You can try this yourself by wrapping the call to getAddressDomain(callback) in a try catch block and then experimenting with an exception placed before line 8 and then try it again with the exception moved to before line 12.

So, what can we do about this. It really depends upon whether we are in control of how our code is called. If we are in control then we simply do not wrap calls to our async functions in try catch blocks. If we are not in control, and really we never know what might happen in the future, then we need to make sure our function still works properly if called within a try catch block. This is done simply by converting any of our synchronous code to async code as below.

function getAddressDomain(callback){  
    var d = domain.create().on("error", function(err) {  
        //We don't need to worry about process.nextTick due to the way domains work  
        return callback(err);  
    });  
    // A trivial example of some synchronous work that might throw an exception  
    d.enter();  
    process.nextTick(function(){  
        var domainName = "nodejs" + ".org";  

        dns.lookup(domainName, 4, d.intercept(function(address){  
                //This is some other code that might throw an exception  
                console.log(address);  

                return callback(null, address);  
        }));  
    });  
    d.exit();  

}  

So really, this is the safest way to use domains within your functions. I am planning to write at least two more blog posts about domains. One about when to use domain.dispose(); and another on writing a helper class that simplifies the use of domains in async functions as above.

Ben Sudbury Avatar

Ben Sudbury

For more information about Ben Sudbury check the bio at the link below.

http://www.lighthouselogic.com/about-me/

comments powered by Disqus