14

To make debugging easier, I'm capturing all of the console logs in Chrome so that users who submit a feedback entry will also submit all of the logs to our server. When someone encounters a problem in production, I can first and foremost get them back to work so that I can then sit down and more thoroughly go through all of the logs to determine the root cause of whatever issue the user encountered in production.

The technique I use to capture the logs involves overriding console.log so that all text entered in the first argument gets stored in an array while simultaneously invoking the legacy function so that I can still see the logs in the console too.

The problem is when there's the occasional uncaught exception. These aren't included in the uploaded logs, so it's not always clear what caused the problem. So I tried overriding ReferenceError by writing a JavaScript function that takes a function as an argument, then returns a new function that does stuff with it, like storing data in a variable, and then invoking the legacy function as the last step:

function overrideException(legacyFn) {  

    /** arguments for original fn **/
    return function() {

        var args = [];

        args[0] = arguments[0];

        // pass in as arguments to original function and store result to 
          // prove we overrode the ReferenceError
        output = ">> " + legacyFn.apply(this, args).stack;

        return legacyFn.apply(this, arguments);
    }           

}

To test the overrideException function, I ran the following code on the console:

ReferenceError = overrideException(ReferenceError);

Afterwards, I tested the returned function, the new ReferenceError, by manually throwing a ReferenceError:

throw new ReferenceError("YES!! IT WORKS! HAHAHA!");

The resulting output on the console is:

ReferenceError: YES!! IT WORKS! HAHAHA!

And checking the global variable output from the overrideException function shows that it did indeed run:

output
  ">> ReferenceError: YES!! IT WORKS! HAHAHA!
  at ReferenceError (<anonymous>)
  at new <anonymous> (<anonymous>:18:35)
  at <anonymous>:2:7
  at Object.InjectedScript._evaluateOn (<anonymous>:562:39)
  at Object.InjectedScript._evaluateAndWrap (<anonymous>:521:52)
  at Object.InjectedScript.evaluate (<anonymous>:440:21)"

Now, here's where things start to fall apart. In our code, we're not going to know when an uncaught exception occurs, so I tested it by attempting to run a function that doesn't exist:

ttt();

Which results in:

ReferenceError: ttt is not defined

However, unlike the case where we explicitly throw an error, in this case, the function doesn't fire, and we're left with only the legacy functionality. The contents of the variable output is the same as in the first test.

So the question seems to be this: How do we override the ReferenceError functionality that the JavaScript engine uses to throw errors so that it's the same one we use when we throw a ReferenceError?

Keep in mind that my problem is limited only to Chrome at this time; I'm building a Chrome Packaged app.

jamesmortensen
  • 33,636
  • 11
  • 99
  • 120
  • Guessing you don't want to wrap your code in a `try/catch`, and process the error object then re-throw there? –  Jul 18 '13 at 01:37
  • @CrazyTrain - I could do that, and I probably will. However, it's tedious and also possible to miss something. I like solutions that are dummy-proof and all-inclusive because they generally are also "I'm too busy-proof" :) My plan is of course to use more try/catches, but this seemed like such an excellent solution, so I started digging into it. – jamesmortensen Jul 18 '13 at 01:40
  • I mean one monolithic `try/catch` that wraps all your code. Since it seems the goal isn't so much to deal with errors in place, but rather to reformat them somehow, then wrapping all the code in a single `try/catch` would seem little different than your current approach of replacing the constructor. You'll just need to be certain that you re-throw whatever errors you get. –  Jul 18 '13 at 01:53
  • @CrazyTrain - I did think about that, but then the entire app would stop working... hmmm... or would it... I may have to try your suggestion just out of curiosity, but I'm really interested in seeing how to override these native Error objects. :) – jamesmortensen Jul 18 '13 at 01:56
  • @CrazyTrain - That was a creative suggestion, but the problem is that the code inside the try/catch only runs once, when the page initially loads and loads all of the other functions and event handlers. Once that runs, it doesn't run anymore, so each function and event handler would still need its own independent try/catch block. Hope this helps! – jamesmortensen Jul 18 '13 at 02:05
  • Yeah, it wouldn't stop working, but you're right, I didn't consider asynchronous stuff, like handlers. Though if you do all your binding with `addEventListener()`, you could override that to reduce the tedium. But of course there's other types of async code. Perhaps another approach would be to make a function factory for your async handlers. That way all you'd need to do is pass your handlers to it, and use the returned function, which would wrap the original in the `try/catch`. –  Jul 18 '13 at 02:16
  • ...WRT the actual question, unless Chrome has something special, I sort of doubt that it's possible, but ultimately I just don't know. It would seem similar to wanting to invoke the `Array` constructor every time an `Array` is created using its literal syntax. But hey, maybe Chrome has something for that as well! –  Jul 18 '13 at 02:19
  • @CrazyTrain - Sounds like I'd be traveling too far down the rabbit hole at that point. My goal is to create something simple someone could reuse, and if we have to re-architect the app, then I'm going to have a lot of trouble gaining adoption because they'd have to re-architect their app too. – jamesmortensen Jul 18 '13 at 02:19
  • @CrazyTrain - I've succeeded with the console logs, so some things can definitely be overridden. It's just a matter of finding someone who knows enough about Chrome's internals to get this started. :) – jamesmortensen Jul 18 '13 at 02:21
  • Just a quick note about `try/catch`: don't use it too extensively because it [performs poorly on V8](http://blog.dubbelboer.com/2012/04/16/node-try-catch.html). `window.onerror` is able to catch some errors (as mentioned [here](http://stackoverflow.com/a/5328206/1143495)), but `ReferenceError`, sadly, is not one of them. – Konrad Dzwinel Jul 19 '13 at 08:22
  • Since you are making a packaged app you may also try `chrome.runtime.lastError` ([docs](http://developer.chrome.com/apps/runtime.html#property-lastError)). – Konrad Dzwinel Jul 19 '13 at 08:46

1 Answers1

12

I have done quite a bit of research for the same reason: I wanted to log errors and report them.

"Overriding" a native type (whether ReferenceError, String, or Array) is not possible.

Chrome binds these before any Javascript is run, so redefining window.ReferenceError has no effect.

You can extend ReferenceError with something like ReferenceError.prototype.extension = function() { return 0; }, or even override toString (for consistency, try it on the page, not the Dev Tools).

That doesn't help you much.

But not to worry....

(1) Use window.onerror to get file name, 1-indexed line number, and 0-indexed position of uncaught errors, as well as the error itself.

var errorData = [];
onerror = function(message, file, line, position, error) {
    errorData.push({message:message, file:file, line:line, position:position, error:error});
};

See the fiddle for an example. Since the OP was Chrome-specific, this has only been tested to work in Chrome.

(2) Because of improvements to (1), this is no longer necessary, but I leave this second technique here for completeness, and since onerror is not guaranteed to work for all errors on all browsers. You will also sometimes see the following:

var errors = [];
function protectedFunction(f) {
    return function() {
        try {
            f.apply(this, arguments);
        } catch(e) {
            errors.push(e);
            throw e;
        }
    };
}
setTimeout = protectedFunction(setTimeout);
setInterval = protectedFunction(setInterval);
etc...

FYI, all this is very similar to what has been done in the Google Closure Compiler library, in goog.debug, created during Gmail development with the intent of doing exactly this. Of particular interest is goog.debug.ErrorHandler and goog.debug.ErrorReporter.

Paul Draper
  • 78,542
  • 46
  • 206
  • 285
  • 1
    You sir, are a genius! I tried it out and actually had a real uncaught exception in my code I didn't know about, and it got logged to my log framework! I'm going to need to play around with this a bit more, as I noticed it didn't work inside certain callbacks. I'm hoping your setTimeout/setInterval approach will work for those cases. +1 – jamesmortensen Oct 29 '13 at 06:16
  • Thanks. (1) should work for all uncaught exceptions (whether from script evaluation, event listeners, etc.). Let it be known if you find cases where it doesn't work. – Paul Draper Oct 29 '13 at 06:54
  • I'll have to play around with it more tomorrow, and I may have just made a mistake, but I recall some other event handler throwing the errors generated inside chrome.app.window.create, instead of window.onerror handling them. I'm trying this inside a Chrome packaged app `create` method callback function. – jamesmortensen Oct 29 '13 at 07:03
  • `chrome.app.window.create` seems to cause `onerror` for me; just remember to set `onerror` on the correct window. Since Chrome Apps have a background page, make sure to set it there too. – Paul Draper Oct 29 '13 at 09:12
  • Oh, and I found that we *can* get the original error -- and thus the stack trace -- in `onerror` (see updated answer and jsFiddle). – Paul Draper Oct 29 '13 at 09:12