SBel
SBel

Reputation: 3359

JavaScript infrastructure such that propagated errors don't get lost

My group is starting a new project. We are thinking about organizing the JavaScript in such a way so that any JavaScript errors don't get lost but rather get caught and sent to the server to be logged. For namespacing I want to keep it simple so I'm using something like this:

var my_namespace = function() {
    function myFunction(input) {
        if (input < 0) {
            throw "input must be positive";
        }

        return 'result';
    }

    return {myFunction: myFunction};
} ();

So now I can invoke my_namespace.myFunction(-22) but when the error will be thrown it will get lost. There will be many namespaces each one in it's own .js file (maybe somebody has a better idea about namespace schema).

So, my question is, how to like "surround" namespaces so that errors will not get lost?


Actually @Relic gave a good idea. I'm going to write below the code that will create the namespace "my_namespace" and surround the initialization by jQuery with try-catch:

var my_namespace = function() {
    function init() {
        throw "an exception during initialization";
    }

    return {init: init};
} ();

$(document).ready(function() {
    try {
        my_namespace.init();
    } catch (e) {
        // handle error
    }
});

I'm going to experiment with what happens after it does initialization, that is, with the event handling.


Yep, just as I thought, event handling exceptions will not be caught. I'll research some more and return.

Upvotes: 2

Views: 1169

Answers (4)

SBel
SBel

Reputation: 3359

After spending some time looking for the solution I came to the following conclusion. You must use window.onerror, there is no other way.

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074495

Two options for you:

Wrap Everything

You can wrap all of your code with try/catch blocks. This isn't as tedious as it sounds. There are two aspects of this: Wrapping your main code, and wrapping code that runs in response to events (user events, timer events, etc.). You can either do that manually, or you can give yourself a framework for doing it.

This doesn't have to be a pain at all. For instance, for the first part, just wrap a try/catch around your main code:

(function() {   // (If you don't use scoping functions, just ignore this and the last line

    try {
        // Your code here
    }
    catch (e) {
        reportException(e);
    }

    function reportException(exception) {
        try {
            // Do whatever you want to do to report the exception here.
        }
        catch (e) {
            // Let the browser report it
            throw 'Error handling exception: ' + exception;
        }
    }

})();

For the second part (catching exceptions in event handlers and code fired with setTimeout and similar), you can either always manually use try/catch blocks in all of your code (which is frequently what you want to do anyway), and possibly use a central function that wraps your event handlers to make sure uncaught exceptions are caught and handled, like this:

function makeHandler(handler) {

    eventHandler.original = handler;
    return eventHandler;

    function eventHandler(event) {
        try {
            // Trigger the handler
            return handler.call(this, event);
        }
        catch (e) {
            // Handle event handler exception
            reportException(e);
        }
    }
}

(There are more features you might add to that, but those are the basics.)

For public methods, you can use something quite similar to makeHandler:

function makePublic(method) {

    publicMethod.original = method;
    return publicMethod;

    function publicMethod() {
        try {
            // Trigger the actual member
            return method.apply(this, arguments);
        }
        catch (e) {
            // Handle reporting the exception
            reportException(e);

            // Then probably re-throw it so the calling code
            // sees it
            throw e;
        }
    }
}

Bringing that all together, this code:

var Namespace = (function() {
    var NS = {};

    // Some setup
    doSomething();
    doSomethingElse();
    if (/* Some condition */) {
       doYetAnotherThing();
    }

    // Export public methods
    NS.foo = foo;
    NS.bar = bar;

    function doSomething() {
        var element = document.getElementById("foo");
        // Note, next line could throw if element doesn't exist
        element.addEventListener("click", function(event) {
            // Handling click
            var other = element.getElementsByTagName('input')[0];
            element.innerHTML = other.value; // Could throw if `other` not there
        }, false);
    }

    // ...other functions, including `foo` and `bar`...

    // Return the namespace object
    return NS;
})();

Turns into:

var Namespace = (function() {
    var NS = {};

    try {
        // Some setup
        doSomething();
        doSomethingElse();
        if (/* Some condition */) {
           doYetAnotherThing();
        }

        // Export public methods
        NS.foo = makePublic(foo);
        NS.bar = makePublic(bar);
    }
    catch (e) {
        reportException(e);
    }

    function doSomething() {
        var element = document.getElementById("foo");
        // Note, next line could throw if element doesn't exist
        element.addEventListener("click", makeHandler(function(event) {
            // Handling click
            var other = element.getElementsByTagName('input')[0];
            element.innerHTML = other.value; // Could throw if `other` not there
        }), false);
    }

    // ...other functions, including `foo` and `bar`...

    // ...`reportException`, `makeHandler`, `publicMethod`...

    // Return the namespace object
    return NS;
})();

So it's not that much impact.

You always want to use more targeted try/catch as part of your logic, but you can also use these global try/catch blocks to catch anything unexpected (like those silly bugs that sometimes slip into the catch block!), etc.

There are several advantages to this approach:

  • It works on all browsers.

  • You can throw things other than strings (more structured exceptions), and they'll still be objects when you catch them.

  • If an error reaches the browser level, you know it's not in your code, or it's in your exception reporting code.

Use window.onerror

If for whatever reason the above isn't to your taste, a little-known feature of the browser environment is the fact that you can trap any uncaught error by assigning a function to window.onerror (live example described and linked below):

window.onerror = globalErrorHandler;

function globalErrorHandler(errorMsg, url, lineNumber) {
    // Do something with the error here
}

This works in most browsers, but not all, and suffers from the fact that chaining these sorts of error handlers isn't natively supported (you have to do it yourself) and by the time the error reaches your code, it's already been turned into a string (a pain if you're using more structured exception objects).

Details on the MDC page for it, including how to play nice with others; slightly modified example:

function addWindowErrorHandler(handler) {
    var previous = window.onerror;
    window.onerror = function(errorMsg, url, lineNumber) {
        var returnValue = false,
            handled = false;

        // Call the handler
        try {
            returnValue = handler(errorMsg, url, lineNumber);
        }
        catch (e) {
            // Eat the error
        }

        // Hand off to previous
        if (!returnValue && previous) {
            try {
                returnValue = previous(errorMsg, url, lineNumber);
            }
            catch (e) {
                // Just eat it
            }
        }

        // Done
        return returnValue;
    };
}

Just call that with a reference to your handler function, and have your handler function return true if the error was yours to handle, false otherwise.

To know whether the error is yours or not, you might consider putting a marker in the string (sadly, it'll be a string by the time it reaches the onerror handler, even if you threw some other object type). So you might use a worker function for the whole module that adds a marker, e.g.:

function myException(msg) {
    return '*Marker* ' + msg;
}

Then

throw myException('cannot be negative');

and your handler would do

if (String(error).indexOf('*Marker*') >= 0) {
    // It's ours

    // ...handle it...

    // Flag that we handled it
    return true;
}

Unfortunately, even though you process the error, I'm not aware of any way to suppress it (the script still stops executing at that point).

You could even have Exception objects you construct that accept a message and a nested exception if you like. Just be sure to handle toString on them, because (again) by the time the error gets to the error handler, it's already been turned into a string.

Live example

Upvotes: 2

Eric Hodonsky
Eric Hodonsky

Reputation: 5897

My Official answer:

<input type="button" id="btn" name="dont click me" value="dont click me" />
var _ns = { init: function() {
        this.events();
    },
    events: function(){
        $("#btn").on('click mouseover', function(event){
            if(event.type != "mouseover"){
                _ns.error.alert("Annnd you clicked me anyways");
            }else{
                _ns.error.console("nice mouseover skillz");
            }
        });
    },
    error:{
        console:function (Error) {
            console.log("Error: "+Error);
        },
        alert: function(Error){
            alert("Error: "+Error);
        }
    }
};
$(document).ready(function() {
    _ns.init();
});

Upvotes: 0

Rich O&#39;Kelly
Rich O&#39;Kelly

Reputation: 41767

I'd recommend a block of code that can monkey patch any function calls (nb untested):

(function (namespace) {
  var addTryCatch = function (delegate) {
    try {
      delegate.apply(this, arguments);
    } catch {
      // standard code here
    }
  };
  for (var propName in namespace) {
    var prop = namespace[propName];
    if (typeof prop === 'function') {
      namespace[propName] = addTryCatch(namespace[propName]);
    }
  }
}(yourNamespace));

Any recursion could be added if necessary.

Upvotes: 1

Related Questions