Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run a piece of JavaScript as soon as a third-party script fails to load

I provide a JavaScript widget to several web sites, which they load asynchronously. My widget in turn needs to load a script provided by another party, outside my control.

There are several ways to check whether that script has successfully loaded. However, I also need to run different code if that script load has failed.

The obvious tools that don't work include:

  • I'm not willing to use JavaScript libraries, such as jQuery. I need a very small script to minimize my impact on the sites that use my widget.

  • I want to detect the failure as soon as possible, so using a timer to poll it is undesirable. I wouldn't mind using a timer as a last resort on old browsers, though.

  • I've found the <script> tag's onerror event to be unreliable in some major browsers. (It seemed to depend on which add-ons were installed.)

  • Anything involving document.write is right out. (Besides that method being intrinsically evil, my code is loaded asynchronously so document.write may do bad things to the page.)

I had a previous solution that involved loading the <script> in a new <iframe>. In that iframe, I set a <body onload=...> event handler that checked whether the <script onload=...> event had already fired. Because the <script> was part of the initial document, not injected asynchronously later, onload only fired after the network layer was done with the <script> tag.

However, now I need the script to load in the parent document; it can't be in an iframe any more. So I need a different way to trigger code as soon as the network layer has given up trying to fetch the script.

I read "Deep dive into the murky waters of script loading" in an attempt to work out what ordering guarantees I can count on across browsers.

If I understand the techniques documented there:

  • I need to place my failure-handling code in a separate .js file.
  • Then, on certain browsers I can ensure that my code runs only after the third-party script either has run or has failed. This requires browsers that support either:
    • Setting the <script async> attribute to false via the DOM,
    • or using <script onreadystatechange=...> on IE 6+.

Despite looking at the async support table, I can't tell whether I can rely on script ordering in enough browsers for this to be feasible.

So how can I reliably handle failure during loading of a script I don't control?

like image 995
Jamey Sharp Avatar asked Nov 26 '13 02:11

Jamey Sharp


1 Answers

I believe I've solved the question I asked, though it turns out this doesn't solve the problem I actually had. Oh well. Here's my solution:

We want to run some code after the browser finishes attempting to load a third-party script, so we can check whether it loaded successfully. We accomplish that by constraining the load of a fallback script to happen only after the third-party script has either run or failed. The fallback script can then check whether the third-party script created the globals it was supposed to.

Cross-browser in-order script loading inspired by http://www.html5rocks.com/en/tutorials/speed/script-loading/.

var fallbackLoader = doc.createElement(script),
    thirdPartyLoader = doc.createElement(script),
    thirdPartySrc = '<URL to third party script>',
    firstScript = doc.getElementsByTagName(script)[0];

// Doesn't matter when we fetch the fallback script, as long as
// it doesn't run early, so just set src once.
fallbackLoader.src = '<URL to fallback script>';

// IE starts fetching the fallback script here.

if('async' in firstScript) {
    // Browser support for script.async:
    // http://caniuse.com/#search=async
    //
    // By declaring both script tags non-async, we assert
    // that they need to run in the order that they're added
    // to the DOM.
    fallbackLoader.async = thirdPartyLoader.async = false;
    thirdPartyLoader.src = thirdPartySrc;
    doc.head.appendChild(thirdPartyLoader);
    doc.head.appendChild(fallbackLoader);
} else if(firstScript.readyState) {
    // Use readyState for IE 6-9. (IE 10+ supports async.)
    // This lets us fetch both scripts but refrain from
    // running them until we know that the fetch attempt has
    // finished for the first one.
    thirdPartyLoader.onreadystatechange = function() {
        if(thirdPartyLoader.readyState == 'loaded') {
            thirdPartyLoader.onreadystatechange = null;
            // The script-loading tutorial comments:
            // "can't just appendChild, old IE bug
            // if element isn't closed"
            firstScript.parentNode.insertBefore(thirdPartyLoader, firstScript);
            firstScript.parentNode.insertBefore(fallbackLoader, firstScript);
        }
    };
    // Don't set src until we've attached the
    // readystatechange handler, or we could miss the event.
    thirdPartyLoader.src = thirdPartySrc;
} else {
    // If the browser doesn't support async or readyState, we
    // just won't worry about the case where script loading
    // fails. This is <14% of browsers worldwide according to
    // caniuse.com, and hopefully script loading will succeed
    // often enough for them that this isn't a problem.
    //
    // If that isn't good enough, you might try setting an
    // onerror listener in this case. That still may not work,
    // but might get another small percentage of old browsers.
    // See
    // http://blog.lexspoon.org/2009/12/detecting-download-failures-with-script.html
    thirdPartyLoader.src = thirdPartySrc;
    firstScript.parentNode.insertBefore(thirdPartyLoader, firstScript);
}
like image 172
Jamey Sharp Avatar answered Oct 07 '22 13:10

Jamey Sharp