So you're thinking of tracking your JS errors


You write code and (of course) you test it, but stuff still breaks. So it's common practice nowadays to log errors in production with some sort of service. With these services, logging your backend errors from your Rails or Node.js server is a simple task. This kind of error reporting does not prevent or solve bugs; however, it provides two crucial capabilities: knowing when something has gone wrong, and providing clues on how to reproduce/fix it. As more and more application logic moves to the browser with Javascript, it's essential to capture errors emanating from the various client scripts in an application. Many of these services support Javascript, and some are even Javascript-only, but they all have a secret: it turns out that logging errors from the browser is not simple and in some cases, not even possible. In fact, it's nearly impossible to retrieve the most crucial component of an error: the stack trace.

A naïve attempt at getting started

Getting up and running with a system to log browser errors is rather painless in and of itself. You can pick from the existing providers, or you could roll your own. All of the providers simply hook into window.onerror and send that information to their backend in order to render their pretty graphs. It's typically just a question of adding the service's <script> tag to your page (they'll have something for you to copy/paste) and you'll be logging in no time. Unfortunately, you'll also notice that you'll have no stack traces and most of your errors will simply report themselves as Script Errors with no line number or filename. Not entirely useless, but nearly... let's see if we can fix it.

Understanding window.onerror

You won't see the words "stacktrace" on any of the Javascript-only error logging services' home pages, and there is a reason for that — window.onerror. Back in the early days of Javascript, it was decided that onerror would not accept an Error object and it therefore does not give us a stack. Instead, onerror receives three parameters: a message, a file, and a line number. This can be somewhat helpful, but we really do want our stacktrace, and for that we need the Error object itself. While it looks like this will be added to browsers soon, for now we'll have to make do.

Stack Trace or GTFO

Well, of course it's a Script Error!

If you are serving Javascript on your site, your scripts are likely coming from a CDN. As much as this helps your page load times, it is also the reason you are seeing all those Script Errors. For security purposes, browsers will not report errors for scripts from a different origin; they will simply report a ScriptError instead of divulging the true nature of the exception (e.g. ReferenceError). But there's an exception to this policy if you load the script using CORS. In a nutshell, Cross-Origin resource sharing (or CORS) is a spec that lets your scripts (or other assets) sidestep the Same-Origin-Policy. For us, the CORS spec boils down to a request with a header Origin: http[s]:// and the response script with a header Access-Control-Allow-Origin: http[s]:// or Access-Control-Allow-Origin: *. Under these circumstances the browser will provide us with the true error message in window.onerror.

Getting set up with CORS

In the simplest case, all we have to do is add the crossorigin attribute to our remote <script>s to enable the request header and configure your server to enable the response header. There are, of course, caveats:

  • Unfortunately, as of September 2013, this doesn't work in Chrome but it should be released in version 30 or 31. IE does not support the attribute and we don't know if/when it will.
  • If you are using a CDN you will need to configure it to support CORS. In some situations, this can be done via configuration, while in other cases, the CDN will simply proxy through the first set of request headers to your server and cache the response headers forevermore. The danger is obvious; if a request comes in from IE/Chrome before Firefox/Safari, the CDN will cache the CORS-header-free version of the file. In this situation, you'll ideally want to Vary on the Origin header. If this is not possible (e.g., with Cloudfront) you may have to always reply with CORS headers when serving Javascript.
  • If you are getting your script from some other source that you don't control, it's possible that the provider has not enabled CORS at all. If you cannot get CORS to work at all, there is not much you can do short of downloading and hosting the content yourself (although please also poke them about enabling CORS).

So what can we do?

There is no good solution just yet but we have a few options:

  • Take what onerror gives us and run with it. It's really not that bad. We can tell a lot from having just the file, error, and line number.
  • Try/catch all the things! Theoretically, we could wrap every function call in a try/catch and we'd have an error object in the catch block. This is bad; please don't try. You'll have a particularly hard time with asynchronous callbacks anyways. You can put reasonable try/catches where things could really go wrong though.
  • Use a front-end web framework. Some front-end JS frameworks have execution contexts that can capture exceptions and give us a real error object (with a stack!). For example, Angular has $exceptionHandler and Ember has a real onerror function.
  • A bit of everything. When the errors are not in Angular/Ember (or if you aren't using a front-end framework at all) and the onerror message is not clear enough, throw some try/catches around to weed out the issues.
  • Wait for it... The ecosystem is not quite setup for painless JS error logging. In a year or two, browsers will have better onerror support and both browsers and CDN providers will have better CORS support.

Ultimately, getting error reports from your Javascript clients is still way more complex than getting them from an app server, but with clients getting thicker and thicker, it's a problem you'll probably want to tackle. Is your team tracking JS errors? If so, let us know how you're doing it in the comments.