How to capture and analyze JavaScript errors
Front-end engineers know that JavaScript has basic exception handling capabilities. We can throw new Error (), and the browser will throw an exception when we call an API Error. However, it is estimated that the vast majority of front-end engineers have not considered collecting such exception information. Anyway, as long as the refresh does not recur after a JavaScript error occurs, the user can solve the problem through refresh, And the browser will not crash. If it has not happened. This assumption was established before the Single Page App became popular. After the Single Page App is running for a period of time, the status is extremely complex. You may have entered a number of operations before coming here. Do you just need to refresh the Page? Shouldn't the previous operations be completely redone? Therefore, it is necessary to capture and analyze the exception information, and then we can modify the code to avoid affecting the user experience.
Capture exceptions
The throw new Error () We write ourselves can be captured, because we know exactly where the throw is written. However, exceptions that occur when you call the browser API are not so easy to capture. Some APIs throw exceptions in the standard, and some APIs throw exceptions only in some browsers due to implementation differences or defects. For the former, we can also capture through try-catch. For the latter, we must listen to global exceptions and then capture them.
Try-catch
If some browser APIs are known to throw an exception, we need to put the call in try-catch to prevent the entire program from entering the invalid state due to an error. For example, window. localStorage is an API that throws an exception when the write data exceeds the capacity limit. This is also true in Safari's private browser mode.
Copy codeThe Code is as follows:
Try {
LocalStorage. setItem ('date', date. now ());
} Catch (error ){
ReportError (error );
}
Another common use case of try-catch is callback. Because the code of the callback function is uncontrollable, we do not know the code quality and whether to call other APIs that will throw exceptions. In order not to make other code after callback unable to be executed because of a callback error, it is necessary to put the call back into try-catch.
Copy codeThe Code is as follows:
Listeners. forEach (function (listener ){
Try {
Listener ();
} Catch (error ){
ReportError (error );
}
});
Window. onerror
If an exception occurs, try-catch can only be captured through window. onerror.
Copy codeThe Code is as follows:
Window. onerror =
Function (errorMessage, scriptURI, lineNumber ){
ReportError ({
Message: errorMessage,
Script: scriptURI,
Line: lineNumber
});
}
Be careful not to use window. addEventListener or window. attachEvent to listen for window. onerror. Many browsers only implement window. onerror, or only the implementation of window. onerror is standard. Considering that the standard draft defines window. onerror, we can use window. onerror.
Attribute loss
Suppose we have a reportError function to collect caught exceptions and send them to the server for storage in batches for query and analysis. What information do we want to collect? Useful information includes: Error Type (name), error message, script, line, and column) stack ). If an exception is captured through try-catch, the information is on the Error object (supported by mainstream browsers), so reportError can also collect the information. However, if the event function is captured through window. onerror, we all know that this event function has only three parameters, so information other than the three parameters is lost.
Serialize messages
If the Error object is created by ourselves, the error. message is controlled by us. Basically, what we put in error. message is what the first parameter of window. onerror is. (The browser will actually slightly modify it, for example, adding the 'uncaught Error: 'prefix .) Therefore, we can serialize the attributes we are concerned with (for example, JSON. Stringify) and store them in error. message. Then we can read window. onerror and deserialize it. Of course, this is limited to Error objects created by ourselves.
Fifth Parameter
Browser vendors also know the restrictions imposed on the use of window. onerror, so they began to add new parameters to window. onerror. Considering that only row numbers without column numbers do not seem very symmetric, IE first adds the column numbers and places them in the fourth parameter. However, you are more concerned about whether you can obtain the complete stack. So Firefox should put the stack in the fifth parameter. But Chrome said it would be better to put the entire Error object in the fifth parameter. You can read any attribute, including custom attributes. As a result, Chrome is fast, and a new window. onerror signature is implemented in Chrome 30, the standard draft is written like this.
Copy codeThe Code is as follows:
Window. onerror = function (
ErrorMessage,
ScriptURI,
LineNumber,
ColumnNumber,
Error
){
If (error ){
ReportError (error );
} Else {
ReportError ({
Message: errorMessage,
Script: scriptURI,
Line: lineNumber,
Column: columnNumber
});
}
}
Attribute Normalization
The names of the Error object attributes we discussed earlier are all based on the Chrome naming method. However, the names of Error object attributes vary in different browsers, for example, the script file address 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 different attribute names to uniform attribute names. For details, refer to this article. Although browser implementations are updated, it is not difficult to manually maintain such a ing table.
Similar to stack tracing. This attribute saves the stack information when an exception occurs in plain text. Because the text format used by each browser is different, you also need to manually maintain a regular expression, extract the identifier, script, line, and column numbers of each frame from plain text ).
Security restrictions
If you have also encountered an error with the message 'script error. ', you will understand what I am talking about. This is actually a browser's limitation on different origin Script files. The reason for this security restriction is as follows: assume that the HTML returned by an online banking user after logon is different from the HTML displayed by an anonymous user, a third-party website can put the website's URI in the script. in the src attribute. HTML cannot be parsed as JS, so the browser throws an exception, and this third-party website can determine whether the user has logged on by parsing the exception location. For this reason, the browser filters all exceptions thrown by different source Script files. Only the unchanged message 'script error. 'is filtered out, and all other attributes disappear.
For websites of a certain scale, it is normal that script files are stored on the CDN, with different origins. Now, even if you build a small website, common frameworks such as jQuery and Backbone can directly reference versions on the public CDN to accelerate user download. Therefore, this security restriction does cause some trouble. As a result, the exception information we collect from Chrome and Firefox is useless 'script error .'.
CORS
To bypass this restriction, you only need to ensure that the script file and the page itself are the same source. However, if you place a script file on a server without CDN acceleration, will the download speed be reduced? One solution is to store the script file on CDN, use XMLHttpRequest to download the content through CORS, and then create the <script> tag to inject it into the page. The embedded code on the page is of course the same source.
This is easy to say, but there are many details to implement. In a simple example:
Copy codeThe Code is as follows:
<Script src = "http://cdn.com/step1.js"> </script>
<Script>
(Function step2 (){})();
</Script>
<Script src = "http://cdn.com/step3.js"> </script>
We all know that if this step 1, step 2, and step 3 have dependencies, they must be executed strictly in this order; otherwise, errors may occur. The browser can request files in step 1 and Step 3 in parallel, but the execution sequence is guaranteed. If we use XMLHttpRequest to retrieve the file content of step 1 and step 3, we need to ensure that the order is correct. In addition, do not forget step 2. When Step 1 is downloaded in non-blocking form, step 2 can be executed. Therefore, we must manually intervene in step 2 and wait until step 1 is complete before execution.
If we already have a set of tools to generate the <script> tag for different pages on the website, we need to adjust this tool to make changes to the <script> tag:
Copy codeThe Code is as follows:
<Script>
ScheduleRemoteScript ('HTTP: // cdn.com/step1.js ');
</Script>
<Script>
ScheduleInlineScript (function code (){
(Function step2 (){})();
});
</Script>
<Script>
ScheduleRemoteScript ('HTTP: // cdn.com/step3.js ');
</Script>
We need to implement the scheduleRemoteScript and scheduleInlineScript functions, and ensure that they are defined before the <script> label of the first external script file is referenced, then the remaining <script> labels are rewritten to the above format. Note that the originally executed step2 function is placed in a larger code function. The code function will not be executed. It is just a container, so that the original step2 code can be saved without escaping, but will not be executed immediately.
Next, we need to implement a complete mechanism to ensure that the file content downloaded by scheduleRemoteScript Based on the address and the Code directly obtained by scheduleInlineScript can be executed one by one in the correct order. The detailed code is not provided here. If you are interested, you can implement it yourself.
Row number lookup
Getting content through CORS and injecting code into the page can break through security restrictions, but it introduces a new problem, that is, line number conflict. Originally, error. script can be used to locate a unique script file, and then error. line can be used to locate a unique row number. Because the code is embedded on the page, multiple <script> labels cannot pass error. script. However, the line numbers in each <script> tag are counted from 1, so we cannot use the exception information to locate the source code location where the error is located.
To avoid line number conflicts, we can waste some line numbers so that the row numbers used by the actual code in each <script> tag do not overlap with each other. For example, if the actual code in each <script> tag does not exceed 1000 lines, I can occupy 1st-1000 lines of code in the first <script> tag, let the code in the second <script> label occupy 1,001st-2000 rows (the first 1000 rows are inserted blank lines ), the third <script> tag code occupies 2001st-3000 rows (the first 2000 rows are inserted blank lines), and so on. Then we use the data-* attribute to record the information for reverse query.
Copy codeThe Code is as follows:
<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>
After such processing, if the error. line of an error is 3005, it means that the actual error. script should be 'HTTP: // cdn.com/step3.js', and the actual error. line should be 5. We can do this row number lookup in the previously mentioned reportError function.
Of course, because we cannot ensure that each script file contains only 1000 rows, some script files may be significantly smaller than 1000 rows, therefore, you do not need to assign a fixed interval of 1000 rows to each <script> tag. We can allocate intervals based on the actual number of script lines, as long as the intervals used by each <script> tag do not overlap each other.
Crossorigin attribute
The security restrictions imposed by the browser on different source content are not limited to <script> tags. Since XMLHttpRequest can use CORS to break this restriction, why can't resources referenced by tags be used directly? Of course this is acceptable.
The <script> label reference of a script file with different origins also applies to the label reference of a file with different origins. If a label is not the same source, the <canvas> label changes to write-only status once it is used in the <canvas> drawing, ensure that the website cannot use JavaScript to steal unauthorized image data of different origins. Later, the label solved this problem by introducing the crossorigin attribute. If crossorigin = "anonymous" is used, it is equivalent to anonymous CORS. If 'crossorigin = "use-credentials" is used, it is equivalent to a CORS with authentication.
Since the label can do this, why can't the <script> label do this? As a result, the browser vendor adds the same crossorigin attribute to the <script> label to solve the preceding security restrictions. Currently, Chrome and Firefox have no problem in supporting this attribute. Safari treats crossorigin = "anonymous" as crossorigin = "use-credentials". The result is that if the server only supports anonymous CORS, Safari fails to perform authentication. Because the CDN server is designed to return only static content for performance consideration, it is impossible to dynamically return the HTTP Header required for CORS authentication based on the request. Safari cannot use this feature to solve the above problem.
Summary
JavaScript Exception Handling seems very simple. It is no different from other languages, but it is not that easy to capture exceptions and analyze attributes. Although some third-party services provide Google Analytics services that catch JavaScript exceptions, you must do it yourself to understand the details and principles.