DEV Community

Vladimir Elchinov for Session Replay

Posted on

"Script error." Is Not an Error. It Is Your Error Tracker Being Told to Shut Up.

If you have ever opened your error dashboard and found a pile of entries reading Script error. with no message, no stack, and line 0, column 0, you have probably filed it under "browser noise" and moved on.

It is not noise. It is a real exception, thrown by real code, with everything useful deliberately removed before it reached you. And the class of code it happens to is the class most likely to break in ways you did not anticipate: the scripts you serve from somewhere other than your own origin.

What is actually happening

The HTML specification gives every classic script a muted errors boolean. The wording is worth reading directly:

if true, means that error information will not be provided for errors in this script. This is used to mute errors for cross-origin scripts, since that can leak private information.

And the condition that sets it:

Let mutedErrors be true if response was CORS-cross-origin, and false otherwise.

That is the whole mechanism. A script fetched from another origin, without CORS, is a script whose exceptions arrive at window.onerror stripped: a fixed message, no filename, no line, no column, and error set to null so there is no stack to walk.

The reasoning is sound. An error message can contain anything the script was holding when it threw. If any page could load any cross-origin script and read its exception text, that is a side channel for reading things the origin never agreed to share. So the browser closes it by default.

The problem is not the rule. The problem is that almost nobody realises the rule applies to them.

Who this actually hits

Not "third-party widgets". You.

The moment your own bundle is served from a CDN on a different hostname than your page - which is most production setups, and is exactly what every performance guide tells you to do - your application's own JavaScript is cross-origin to your application. Every uncaught exception it throws is a candidate for muting.

Which produces the failure mode worth naming: your error tracking still works. You keep getting errors, because inline scripts and same-origin scripts report normally. You just quietly stop getting them from the one bundle that contains most of your code. Nothing announces the gap. Your dashboard is not empty, it is filtered, and the filter is invisible.

The fix is two lines and both are required

On the tag:

<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>
Enter fullscreen mode Exit fullscreen mode

And on the response, from the CDN:

Access-Control-Allow-Origin: https://www.example.com
Enter fullscreen mode Exit fullscreen mode

MDN is blunt about what happens without them: with no crossorigin attribute, "access to error logging via window.onerror will be limited."

Both halves are required, and this is where it usually goes wrong. Adding crossorigin to the tag while the CDN sends no matching header does not get you better errors - it makes the request a CORS request that then fails, so the script does not load at all. A half-applied fix is worse than none, which is why this tends to get tried once, break something, and get reverted.

How to tell whether you are affected, in about a minute

Do not go looking in the dashboard for Script error., because absence there proves nothing. Cause one instead:

// In the console, on your production page:
const s = document.createElement('script');
s.src = 'https://cdn.example.com/does-not-matter.js';
s.textContent = 'throw new Error("canary")';
document.head.appendChild(s);
Enter fullscreen mode Exit fullscreen mode

Better still, add a deliberate throw to a debug entry point in your real bundle, ship it behind a query flag, and read what your tracker records. If what arrives is Script error. at 0:0, every production exception from that bundle has been arriving the same way, for as long as the bundle has been on that hostname.

Why I care about this more than the fix

The interesting part is not the CORS header. It is that this is a monitoring system that fails by returning less, not by returning nothing.

An outage in your error tracker is obvious. A tracker that keeps working while silently dropping one category of error is not, because every check you would run against it comes back healthy. The errors are being reported. They are just being reported as nothing.

That shape shows up all over debugging: the browser has the information, something between you and it decides you may not have it, and what reaches you is a summary that looks like an answer. When somebody tells you the page is broken and your tools show nothing, "nothing" deserves a second question. It may mean nothing happened. It may mean the thing that happened was not allowed to tell you.

Those two look identical from where you are standing, and only one of them is good news.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.