By Ryan Morr
The try/catch block is a unique construct, both in how it works and what it is capable of. Fundamentally, it is able to isolate one or more statements to capture and suppress any runtime errors that may be encountered as a result of execution. It is such a powerful construct that in a perfect world you would want to wrap everything in a try/catch block to provide simple and effective error trapping. However, due to concerns in performance critical situations, employing it is often frowned upon in JavaScript. But what if I told you there was a means of emulating the try/catch block in JavaScript without the concern for performance? This article will be exploring just such a method.
What makes this construct unique is in the manner in which the catch block augments the scope chain. Rather than creating a new execution context and pushing it to the top of the execution stack, the catch block will actually create a new variable object and place it ahead of the activation object in the scope chain of the current execution context. This creates what is known as a dynamic scope, similar to the effect of the with
statement, which lends to its bad reputation as well. As a result, the error
object passed to the catch block does not exist outside of it, even within the same scope. It is created at the start of the catch
clause and destroyed at the end of it. This type of manipulation of the scope chain is the primary reason for the performance hit.
At this point you may be thinking that as long as an error is not raised than performance should not be affected. That is a fair assumption, but you’d be wrong. Some JavaScript engines, such as V8 (Chrome) do not optimize functions that make use of a try/catch block as the optimizing compiler will skip it when encountered. No matter what context you use a try/catch block in, there will always be an inherent performance hit, quite possibly a substantial one.
These limitations are well documented, for instance, look at the following test cases: http://jsperf.com/try-catch-block-performance-comparisonand http://jsperf.com/try-catch-block-loop-performance-comparison. The former confirms that not only is there up to a 90% loss in performance when no error even occurs, but the performance is significantly worse when an error is raised and control enters the catch block. The latter test case proves that the loss is compounded in loops, where most performance intensive operations typically occur.
To find a suitable alternative, we require a reliable means of error notification. For this, there is really only one viable source in the browser we can turn to for error trapping, that being window.onerror
. The question is, can we leverage the event as a means of closely mimicking the functionality of a try/catch block? The answer is yes…for the most part. The event is capable of detecting runtime errors including any errors that you explicitly throw yourself. We can also imitate the error suppression of a catch
block by returning false from window.onerror
, while returning true allows the error to propagate to the browser.
With these advantages, creating a custom alternative that is capable of simulating the primary functionality of a try/catch block becomes rather easy. In its simplest form, the solution involves temporarily caching a catch
function before invoking a try
function, then quickly nullifying the catch
function after the try
function has completed execution. This allows us to reliably resolve the source of an error when one is encountered. We then invoke the associated catch
handler passing an error object constructed from the message, file name, and line number provided to the window.onerror
event handler:
(function(win){
"use strict";
var callback = null, handler = win.onerror;
win.tryCatch = function(tryFn, catchFn){
callback = catchFn;
tryFn();
callback = null;
};
win.onerror = function(msg, file, line){
var error = new Error(), suppress;
error.message = msg;
error.fileName = file;
error.lineNumber = line;
if(callback){
suppress = callback(error);
callback = null;
return suppress === false ? false : true;
}
return handler ? handler.apply(win, arguments) : false;
};
})(this);
The function, appropriately named tryCatch
, allows us to wrap error-prone or sensitive code in a similar fashion as a try/catch block. The syntax is similar as well, the exception being the use of functions instead of block statements, such as the following:
tryCatch(function(){
// try something
}, function(error){
// handle error
});
One of the advantages to this approach is the countless ways we can augment and expand the method to suit various needs. For example, we could add greater debugging capabilities by storing additional information about the source of an error such as the test function itself or any variables the function makes use of. This could help expand error logging to better understand where and why an error took place.
When we measure the performance of this custom solution against a try/catch block, we get some interesting results. Based on some preliminary tests, I found performance comparisons between the browsers fairly inconsistent. Firefox, IE, and Opera all showed improved performance using the tryCatch
function as opposed to a try/catch block, while the results were opposite for Chrome and Safari. However, when we avoid needlessly creating anonymous functions for every invocation of the tryCatch
function and instead use predefined functions, performance actually improves quite a bit in all browsers. Check out the results and try it yourself at: http://jsperf.com/native-try-catch-vs-custom-try-catch/7. The real advantages come inside loops, in this case performance increased dramatically in most browsers: http://jsperf.com/native-try-catch-vs-custom-trycatch-loop.
Despite some of the advantages to this approach, it is not without its caveats. Most of these disadvantages are closely related to the window.onerror
event and can be avoided if used properly in the right circumstances.
One such disadvantage is when the catch
handler is invoked in the case of an error and another error occurs within the handler, then both errors will propagate to the browser. This happens because control still hasn’t left the window.onerror
event handler from the initial error, raising another error will stop execution and fall back to the default behaviour.
There is also a lack of information available beyond the message, file name, and line number of the error provided to the window.onerror
event handler. We don’t get the error type contained in the name property of an error object, although Firefox does prepend this to the message itself. We also don’t get a stack trace, simple reason being that a stack trace is related to an execution context, whereas window.onerror
is invoked in the global execution context. These omissions do limit our ability to more effectively debug an error.
Another drawback inherent to window.onerror
is that once an error is encountered, the browser will stop execution following the invocation of the event handler. Any code following the tryCatch
call will be skipped as the interpreter will instead proceed to the next script block (<script></script>
). This is unavoidable. Only a catch
block is capable of truly suppressing an error without halting execution. Of course, if no further execution is required than this drawback is irrelevant.
Perhaps the greatest disadvantage to this solution are the question marks surrounding window.onerror
. For example, it’s not even clear what errors trigger the event and which don’t. The official Mozilla documentation supports this:
Note that some/many error events do not trigger
window.onerror
, you have to listen for them specifically.
Unfortunately, Mozilla doesn’t get any more specific in terms of which errors are actually caught by window.onerror
. According to the Internet Explorer documentation, the following errors trigger the event:
- Run-time script error, such as an invalid object reference or security violation.
- Error while downloading an object, such as an image.
- Windows Internet Explorer 9. An error occurs while fetching media data
This seems to be consistent with most browsers, but again, not confirmed. While it is easy to test for which errors currently trigger the event, it is the lack of a clear definition for what should trigger it that makes for the problem. Instead, each browser is left to their own devices to decide what should and should not constitute the triggering of window.onerror
. With that type of implementation, how can we trust such an event in every scenario moving forward?
When I initially began experimenting with this method, I was looking for a means of replacing a try/catch block for performance benefits. However, in the end, it started to dawn on me that perhaps the greatest advantage to this solution is in the numerous opportunities for customization. The ability to tailor the tryCatch
function for different use cases and meet various requirements you otherwise couldn’t achieve with a try/catch block could prove to be a more valuable means of error trapping and debugging.
You could look at it as a suitable median. On one hand, you have a method that mimics a try/catch block but has greater opportunities in terms of functionality while also being more beneficial in performance critical situations in some circumstances. On the other hand, it can narrow the scope of a window.onerror
event to specific fragments of code for more effective debugging. Kind of a best of both worlds mashup.
Now let me be clear, I am absolutely not advocating that you replace all your try/catch blocks using this method. This is still very much in the experimental stage, relying on this method in any type of production environment would be premature. There are just too many questions that need answering.
I would, however, encourage you to fork the code on GitHub. Tweak it, improve it, test it out in scenarios I haven’t even thought of, and maybe we can learn more.
This article was originally published at http://ryanmorr.com/reinventing-the-try-catch-block/