Front-end engineers know that JavaScript has basic exception handling capabilities. We can throw new Error(), and the browser will throw an exception if we fail to call the API. But most front-end engineers probably don’t consider collecting such exception information. Anyway, as long as the JavaScript error does not refresh, then the user can fix the problem by refreshing, the browser will not crash, if it did not happen. That assumption was true before Single Page apps became popular. The current Single Page App is in a very complicated state after running for a while, and the user may have to enter several things to get there. Shouldn’t the previous operation be completely redone? It is important to catch and analyze these exceptions so that we can modify the code to avoid impacting the user experience.

The way exceptions are caught

Throw new Error() can be caught if we want, because we know exactly where the throw is written. However, exceptions that occur when a browser API is called are not always easy to catch. Some apis are written in the standard to throw exceptions, and some apis are only thrown by individual browsers because of implementation differences or defects. For the former we can also catch by try-catch, for the latter we must listen for global exceptions and catch them.

try-catch

If some browser apis are known to throw exceptions, we need to put the call in a try-catch to avoid making the whole program illegal. Window. localStorage, for example, is an API that throws an exception when writing data beyond its capacity limit, even in Safari’s private browsing mode.

try {
  localStorage.setItem('date'.Date.now());
} catch (error) {
  reportError(error);
}
Copy the code

Another common try-catch scenario is callbacks. Because the code for the callback function is out of our control, we don’t know what the quality of the code is or whether it calls other apis that throw exceptions. It is necessary to put the call back into a try-catch in order not to cause the rest of the code after the callback to fail because of a callback error.

listeners.forEach(function(listener) { try { listener(); } catch (error) { reportError(error); }});Copy the code

window.onerror

If an exception occurs where a try-catch cannot be covered, it can only be caught by window.onError.

window.onerror =
  function(errorMessage, scriptURI, lineNumber) {
    reportError({
      message: errorMessage,
      script: scriptURI,
      line: lineNumber
    });
}
Copy the code

Be careful not to use window.addEventListener or window.attachEvent to listen for window. onError. Many browsers implement only window.onerror, or only the implementation of window.onerror is standard. Since the draft standard also defines window.onerror, we’ll just use window.onerror.

Property loss

Suppose we had a reportError function that collected caught exceptions and then sent them in bulk to server-side storage for query analysis, what information would we want to collect? Useful information includes: error type (name), error message (message), script file address (script), line number (line), column number (stack). If an exception is caught by try-catch, this information is in the Error object (supported by all major browsers), so reportError can also collect this information. But if it is caught through window.onerror, we all know that the event function has only three arguments, so anything other than those parameters is lost.

Serialized message

Error. Message is controlled by us if we create the Error object ourselves. Basically, whatever we put in error.message, the first argument to window.onerror (message) will be whatever. (Browsers do make minor changes, such as adding the ‘Uncaught Error: ‘prefix.) So we can serialize the properties we care about (such as json.stringify) and store them in error.message, and then read them in window.onError and deserialize them. Of course, this is limited to Error objects that we create ourselves.

The fifth parameter

Browser vendors are also aware of the limitations people face when using window.onerror, so they start adding new parameters to window.onerror. Considering that having only the row number without the column number is not very symmetrical, IE first adds the column number and puts it in the fourth argument. However, people are more interested in getting the full stack, so Firefox says it’s better to put the stack in the fifth parameter. But Chrome says it’s better to put the entire Error object in the fifth argument, and you can read any properties you want, including custom properties. As a result, the new Window.onerror signature was implemented in Chrome 30 due to Chrome’s faster actions, resulting in the draft standard being written accordingly.

window.onerror = function( errorMessage, scriptURI, lineNumber, columnNumber, error ) {
  if (error) {
    reportError(error);
  } elseReportError ({message: errorMessage, script: scriptURI, line: lineNumber, column: columnNumber}); }}Copy the code

Property normalization

The names of the Error object properties we discussed earlier are based on Chrome. However, different browsers use different names for Error object properties. For example, the address of a script file is called script in Chrome but filename in Firefox. Therefore, we also need a special function to normalize the Error object, that is, to map the different attribute names to a uniform attribute name. See this article for details. Although the browser implementation will be updated, it should not be too difficult to manually maintain such a mapping table.

A similar format is the stack trace. This property holds a stack of exceptions in plain text, and since the text format varies from browser to browser, you need to manually maintain a regular representation. The identifier, script, line, and column numbers used to extract each frame from plain text.

Security restrictions

If you’ve ever encountered an error with the message ‘Script error.’, you know what I’m talking about. This is actually a browser limitation for different origin Script files. The reason for this security restriction is that a third-party website can put the URI of an online bank in the script.src property, assuming that the HTML returned by the user after login is different from the HTML seen by the anonymous user. HTML, of course, cannot be parsed as JS, so the browser throws an exception, and the third-party site can determine if the user is logged in by parsing the exception’s location. For this reason, the browser filters all exceptions thrown by different source Script files, leaving the message ‘Script Error.’ unchanged and all other attributes gone.

For sites of a certain size, it is normal to have script files on the CDN from different sources. Now even make a small website, common frameworks such as jQuery and Backbone can directly reference the version on the public CDN, speed up the user download. So this security restriction does cause some trouble, causing all the exception messages we collect from Chrome and Firefox to be useless ‘Script error.’

CORS

To get around this limitation, just make sure that the script file and the page itself are the same origin. But putting the script file on a server without CDN acceleration will slow down the user’s download speed. One solution is to keep the script files on the CDN, use XMLHttpRequest to download the content back through CORS, and create

This is simple enough to say, but there are a lot of details to implement. To use a simple example:

<script src="http://cdn.com/step1.js"></script>
<script>
  (function step2() {}) ();</script>
<script src="http://cdn.com/step3.js"></script>
Copy the code

We all know that this step1, step2, step3 if there is a dependency, we must strictly follow this order, otherwise it may be wrong. The browser can request the files of step1 and step3 in parallel, but the order of execution is guaranteed. If we get the contents of the step1 and step3 files ourselves via XMLHttpRequest, we need to ensure that the order is correct ourselves. In addition, do not forget step2, step2 can be executed when Step1 is downloaded in a non-blocking form, so we must also manually intervene Step2 to make it wait for the completion of Step1 before executing.

If we already have a set of tools to generate

<script>
  scheduleRemoteScript('http://cdn.com/step1.js');
</script>
<script>
  scheduleInlineScript(function code() {(function step2() {}) (); });</script>
<script>
  scheduleRemoteScript('http://cdn.com/step3.js');
</script>
Copy the code

We need to implement scheduleRemoteScript and scheduleInlineScript functions and ensure that they are defined before the first reference to the

Next we need to implement a complete mechanism to ensure that the file content downloaded from scheduleRemoteScript by address and the code retrieved directly from scheduleInlineScript are executed one after the other in the correct order. I will not give the detailed code here, you can be interested in their own implementation.

The line number check

Fetching content through CORS and injecting code into the page can overcome security limitations, but introduces a new problem, line number conflicts. Error. Script can be used to locate a unique script file and error. Line can be used to locate a unique line number. Now, because the code is embedded in the page, multiple

To avoid line number conflicts, we can waste some line numbers so that each

<script
  data-src="http://cdn.com/step1.js"
  data-line-start="1"
>
  // code for step 1
</script>
<script data-line-start="1001">
  // '\n' * 1000
  // code for step 2
</script>
<script
  data-src="http://cdn.com/step3.js"
  data-line-start="2001"
>
  // '\n' * 2000
  // code for step 3
</script>
Copy the code

After this processing, if an error line is 3005, that means the actual error. Script should be ‘http://cdn.com/step3.js’ and the actual error. Line should be 5. We can do this in the reportError function mentioned earlier.

Of course, since there is no guarantee that each script file will be only 1000 lines long, and some scripts may be significantly smaller than 1000 lines long, there is no need to assign a fixed 1000 line interval to each

Crossorigin properties

Security restrictions imposed by browsers on content from different sources are certainly not limited to

The same restrictions that apply to

If the tag can do this, why not the

conclusion

JavaScript exception handling may seem simple, just like any other language, but it’s not that easy to actually catch all the exceptions and analyze the properties. Although there are some third-party services that offer A Google Analytics like service for catching JavaScript exceptions, you still have to do it yourself to understand the details and how it works.