Front-end engineers know that JavaScript has basic exception handling capabilities. We can throw the new error () and the browser will throw an exception when we call the API error. But it is estimated that most front-end engineers have not considered collecting these anomaly information.
Anyway, as long as the JavaScript error after the refresh is not present, the user can solve the problem by refreshing, the browser will not crash, when it has not happened well. This assumption was established before the single Page App was popular. Now the single Page APP runs for a period of time after the state is very complex, the user may have a number of input operations to come here, said the refresh is refreshed ah? Do you want to redo all the previous operations? So we still need to capture and analyze these exception information, and then we can modify the code to avoid impacting the user experience.
How to catch exceptions
We write our own throw new Error()
to capture, of course, because we know throw
where to write. However, the exception that occurs when invoking the browser API is not necessarily easy to capture, and some APIs are written in the standard to throw exceptions, and some APIs only throw exceptions because of differences or flaws in the individual browsers. For the former we can also try-catch
catch, for the latter we have to listen to the global exception and then capture.
Try-catch
If some browser API is known to throw an exception, then we need to put the call try-catch
inside, to avoid the error caused by the entire program into an illegal state. For example window.localStorage
, an API that throws an exception after the write data exceeds the capacity limit, as in Safari's private browsing mode.
Try {localstorage.setitem (' Date 'catch (error) {ReportError (Error)}
Another common try-catch
application scenario is callbacks. Because the code of the callback function is we are not controllable, the code quality, will not invoke other throws the exception the API, we do not know. It is necessary to put the call back inside in order not to be able to execute other code after calling the callback because of a callback error try-catch
.
Listeners.foreach (functiontrycatch (error) {ReportError ( Error); }});
Window.onerror
For try-catch
areas not covered, if an exception occurs, it can only be window.onerror
captured by the.
function (ErrorMessage, Scripturi, linenumber) {reportError ({message:errormessage, Script:scripturi, Line:linenumber});}
Be careful not to use window.addEventListener
or window.attachEvent
to listen in a smart way window.onerror
. Many browsers are only implemented window.onerror
, or only window.onerror
implementations are standard. Given the definition of the draft standard window.onerror
, we window.onerror
should use it.
property is missing
Suppose we have a reportError
function that collects the caught exceptions and then sends them to the server-side store for query analysis, what information do we want to collect? Useful information includes: error type (), name
error message ( message
), script file address ( script
), line number (), line
column number ( column
), stack trace ( stack
). If an exception is try-catch
captured, this information is Error
available on the object (supported by the mainstream browser), so reportError
it can be collected. But if it is window.onerror
captured, we all know that this event function has only 3 parameters, so the unexpected information of these 3 parameters is lost.
Serializing messages
If the Error
object is created by ourselves, then error.message
we are controlled by it. Basically what we put into it error.message
window.onerror
is what the first parameter ( message
) will be. (The browser will actually be slightly modified, for example by ‘Uncaught Error: ‘
prefixing it.) So we can serialize the properties we care about (for example JSON.Stringify
) and store them error.message
inside, and then deserialize them in the window.onerror
read. Of course, this is limited to the objects we create ourselves Error
.
Fifth parameter
Browser vendors are also aware window.onerror
of the restrictions that people are using, so start window.onerror
adding new parameters to them. Considering that only the row number does not appear to be very symmetrical, IE first adds the column number, placed in the fourth parameter. However, people are more concerned about the ability to get the complete stack, so Firefox is better to put the stack on the fifth parameter bar. But Chrome says it's better to put the whole Error
object in the fifth parameter, which you want to read, including the custom properties. As a result, chrome moves faster, and new signatures are implemented in Chrome 30 window.onerror
, leading to the standard draft.
functionifelse {reportError ({message:errormessage, Script:scripturi , Line:linenumber, column:columnnumber}); }}
Attribute Normalization
The object properties we discussed earlier are Error
all based on the name of Chrome, but different browsers are Error
naming object properties differently, for example, the script file address is called in Chrome script
but in Firefox filename
. Therefore, we also need a special function to normalize the Error
object, that is, to map different property names to the uniform property names. You can refer to this article for specific practice. Although the browser implementation is updated, it is not too difficult to manually maintain a mapping table.
Similar is the format of the stack trace ( stack
). This property saves a copy of the stack information in plain text as it occurs, and because each browser uses a different text format, manual maintenance of a regular expression is required to extract the function name ( identifier
), file () script
, line number (), line
and column number of each frame from the plain text ( column
).
Security restrictions
If you also encounter ‘Script error.‘
the error of the message, you will understand what I'm talking about, which is actually the browser's limitations on different source (origin) script files. The reason for this security restriction is this: Suppose a net bank returns HTML after the user logs in a different way than the HTML that the anonymous user sees, a third-party site can put the URI of the net-silver into the script.src
attribute. HTML is certainly not possible to be interpreted as JS, so the browser throws an exception, and this third-party site can be resolved by the location of the exception to determine whether the user has logged in. This browser for different source script files thrown by the exception is filtered to only ‘Script error.‘
leave such a constant message, all the other properties disappear.
For a certain size of the site, the script file on the CDN, the non-homologous is very normal. Now it's time to make a small website, and common frameworks like JQuery and Backbone can refer directly to the version on the public CDN to speed up user downloads. So this security limit does cause some trouble, and the exception information we collect from Chrome and Firefox is useless ‘Script error.‘
.
CORS
To circumvent this limitation, just make sure the script file and the page itself are the same. But if you put the script file on a server that does not have CDN acceleration, does it slow down user download speed? One solution is that the script files continue to be placed on the CDN, using XMLHttpRequest
CORS to download the content back, and then create <script>
tags to inject into the page. The code embedded in the page is of course homologous.
This is easy to say, but there are a lot of details to come up with. In a simple example:
<Scriptsrc= "Http://cdn.com/step1.js"></Script><Script> (functionStep2 () {}) ();</Script><Scriptsrc= "Http://cdn.com/step3.js"></Script>
We all know that this step1, STEP2, step3 if there is a dependency, it must be executed strictly in this order, otherwise there may be an error. Browsers can request step1 and step3 files in parallel, but the order is guaranteed at execution time. If we ourselves XMLHttpRequest
obtain Step1 and Step3 's file content, we need to ensure that the order is correct in its own right. Also do not forget the Step2, Step1 in a non-blocking form when the STEP2 can be executed, so we must also human intervention step2 let it wait for Step1 to be completed before execution.
If we already have a complete set of tools to generate labels for the different pages on <script>
our site, we need to tweak the tool to make changes to the <script>
tags:
<Script>Scheduleremotescript ('Http://cdn.com/step1.js');</Script><Script>Scheduleinlinescript (functioncode () {(functionStep2 () {}) (); });</Script><Script>Scheduleremotescript ('Http://cdn.com/step3.js');</Script>
We need to implement the scheduleRemoteScript
scheduleInlineScript
two functions and ensure that they are defined before the first tag that references the external script file <script>
, and then the rest of the <script>
tags are rewritten in this form. Note that the function that step2
was executed immediately is put into a larger code
function. The code
function is not executed, it is just a container, so that the original STEP2 code does not need to be escaped to be preserved, but will not be executed immediately.
Next we need to implement a complete set of mechanisms to ensure that the contents of the scheduleRemoteScript
files downloaded from the address and scheduleInlineScript
directly obtained from the code can be executed one after the other in the correct order. Detailed code I will not be here to give, everyone is interested in the realization of their own.
Counter-Search by line number
Getting content through CORS and injecting code into the page can break the security limit, but introduces a new problem, which is line number collisions. Originally error.script
, you can navigate to a unique script file, and then error.line
you can navigate to a unique line number. Now that the code is embedded in the page, multiple <script>
tags cannot be error.script
distinguished, but the <script>
line numbers inside each label are calculated from 1, resulting in the inability to locate the source code where the error is located.
In order to avoid line number conflicts, we can waste some line numbers, make each <script>
tags have actual code in the line number range does not overlap with each other. For example, assuming each <script>
The actual code in the tag is no more than 1000 lines, then I can make the first <script>
The code in the tag occupies line 1–1000, allowing the second <script>
The code in the tag occupies line 1001–2000 (preceded by 1000 rows of blank lines), and a third <script>
The code for the label takes the line 2001–3000 (insert 2000 lines in front of the line), and so on. We then use data-*
Properties to record this information for easy back-check.
<script data-src= "http://cdn.com/step1.js" data// code for step 1// ' \ n ' * // code for Step 2</script><script data-src= "http://cdn.com/step3.js" data// ' \ n ' *$// code for step 3</script>
After such treatment, if one error.line
is wrong 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 previous function of the reportError
line number counter-search work.
Of course, since we can not guarantee that each script file has only 1000 lines, it is possible that some script files are significantly less than 1000 lines, so there is no need to fix a 1000 line of the interval to each <script>
label. We can allocate intervals based on the number of actual script rows, as long as <script>
the intervals used for each label are guaranteed to be non-overlapping.
Crossorigin Property
The browser's security restrictions on the content of different sources are certainly not limited to <script>
labels. Since XMLHttpRequest
it is possible to break through this limitation through CORS, why is the resource referenced directly through the tag not possible? This is of course possible.
<script>
The restrictions that refer to different source script files for labels also work on
labels referencing different source picture files. If a
label is a different source, once it is used in the <canvas>
drawing, it <canvas>
will become a write-only state, ensuring that the site cannot steal unauthorized, different source image data through JavaScript. Later
crossorigin
, the label solves the problem by introducing attributes. If used crossorigin="anonymous"
, it is equivalent to anonymous cors, and if you use ' crossorigin= ' use-credentials, it is equivalent to a certified cors.
Since
labels can do this, why <script>
can't labels do this? The browser vendor then <script>
adds the same attributes to the tag to address the crossorigin
security restrictions described above. Now Chrome and Firefox support for this property is absolutely no problem. Safari will treat it crossorigin="anonymous"
as crossorigin="use-credentials"
a result if the server only supports anonymous CORS, Safari will fail as authentication. Since the CDN server is designed to return only static content for performance reasons, it is not possible to dynamically return the HTTP Header,safari required to authenticate CORS on request equivalent to the inability to exploit this feature to address the above problem.
Summarize
JavaScript exception handling looks simple, it's no different from other languages, but it's really not that easy to catch the exceptions and then analyze the attributes. Now, while there are some third-party services that provide the kind of Google Analytics services that capture JavaScript exceptions, it's important to do it yourself if you want to figure out the details and rationale.
JS Advanced Debugging Tips: Capturing and parsing JavaScript error details