skip to Main Content

My browser-side JavaScript uses a dynamic import import('/some-url') which sometimes throws an error.

How can I tell whether the thrown error originates from the loaded JavaScript module, or from the browser because no JavaScript module was found at '/some-url'?

The error thrown by the browser if '/some-url' doesn’t return a JavaScript module doesn’t seem to follow any convention:

  • In Chrome, err.message is 'Failed to fetch dynamically imported module: https://stackoverflow.com/some-url'
  • In firefox, err.message is 'error loading dynamically imported module'.

A hackish way to implement this:

async function isJavaScriptModule(url) {
  try {
    await import(url);
  } catch (err) {
    if (isNetworkError(err)) {
      return false;
    } else {
      // The module at `url` may throw an error when loaded.
      // It's still a JavaScript module so we return true.
      return true;
    }
  }
  return true;
}

function isNetworkError(err) {
  const messages = [
    "Failed to fetch dynamically imported module",
    "error loading dynamically imported module",
  ];
  return messages.some((msg) => err.message.includes(msg));
}

But that isn’t reliable, since browser vendors may change the error message at any time.

Is there a more reliable way to implement this?

3

Answers


  1. Id try fetching, then checking if the fetch worked and returned a js module and then importing:

    async function isJavaScriptModule(url) {
      try {
        const response = await fetch(url);
        if (response.headers.get("content-type") === "application/javascript") {
          await import(url);
          return true;
        } else {
          return false;
        }
      } catch (err) {
        return false;
      }
    }
    
    Login or Signup to reply.
  2. You can differentiate between the two error types by checking the error instance. If the URL in the import is incorrect, it will result in a TypeError instance. Conversely, a custom error within the module will be an instance of Error.

    import('http://localhost:3000/wrong-url.js')
      .then(module => console.log(module))
      .catch(error => console.log(error instanceof TypeError)) // 👈 true
    
    import('http://localhost:3000/correct-url.js')
      .then(module => console.log(module))
      .catch(error => console.log(error instanceof TypeError)) // 👈 false
    

    Example with a wrapper function:

    const importScript = url => import(url).catch(error => ({isNetworkError: error instanceof TypeError}))
    
    importScript('./correct.js').then(console.log) // 👉 module
    importScript('./has-exception.js').then(console.log) // 👉 {isNetworkError: false}
    importScript('./wrong-url.js').then(console.log) // 👉 {isNetworkError: true}
    

    The demo can be forked from here

    Login or Signup to reply.
  3. There is indeed no easy way to determine this. The script execution could throw the exact same error you’d get with a network error and you’d have no way of knowing where it came from.

    import(`data:text/javascript,
      throw new TypeError("I can pretend I'm an import error");
    `).catch((err) => 
        console.log(`[${ Object.getPrototypeOf(err).name }]`, err.message)
      );

    So for this you will probably have to fetch again the resource on your own, and perform the same checks that import() does.

    You’ll first want to ensure that you’re allowed to fetch and that the URL is a valid URL. This is handled by the call to fetch(url). If it throws here, your script didn’t execute.
    Then you will have to check if the Response instance’s .ok is true, otherwise the browser will fail to import the module script.
    Finally, you’ll want to ensure the Content-Type of the resource is correctly set to one of the many JS MIME types (assuming you’re loading a JS module).

    This last step is probably the most complicated since this "Content-Type" header can come in many forms. I’ll provide a really quickly made sketch below, but you should really search for a better one (e.g jsdom has one).

    const urls = [
      // expected ok
      ["https://unpkg.com/[email protected]/dist/es-module-shims.js", true],
      // expected ok, even if the module throws (yes, there is a "throw" pkg, and yes it throws as ESM ;)
      ["https://unpkg.com/throw", true],
      // expected not-ok (404)
      ["https://unpkg.com/notok", false],
      // expected Network Error
      ["https://v8.dev/_js/main.mjs", false],
    ];
    
    const isValidJSModule = async (url) => {
      try {
        // We use `mode: "cors"` to perform the same request as `import()`
        // and `method: "HEAD" to avoid fetching the whole resource again
        resp = await fetch(url, { mode: "cors", method: "HEAD" });
        return resp.ok && isJSMIME(resp.headers.get("content-type"));
      }
      catch(err) {
        return false; // or "Network Error" if you prefer
      }
    }
    
    const tryToImport = async (url) => {
      try {
        return await import(url);
      }
      catch(err) {
        return "Failed to import";
      }
    }
    
    const test = async ([url, expected]) => {
      const module = await tryToImport(url);
      // You'd perform it only in case import() failed
      const isValid = await isValidJSModule(url);
      console.log({ url, module, isValid, expected });
    }
    urls.forEach(test);
    
    function isJSMIME(fullType) {
      // this is naive and may very well fail
      const type = fullType.split(";")[0].toLowerCase().trim();
      return [
        "application/ecmascript",
        "application/javascript",
        "application/x-ecmascript",
        "application/x-javascript",
        "text/ecmascript",
        "text/javascript",
        "text/javascript1.0",
        "text/javascript1.1",
        "text/javascript1.2",
        "text/javascript1.3",
        "text/javascript1.4",
        "text/javascript1.5",
        "text/jscript",
        "text/livescript",
        "text/x-ecmascript",
        "text/x-javascript",
      ].includes(type);
    }

    Ps: Note that in Firefox it seems the returned error’s .stack is the empty string when it’s caused by a network error, while it points to the faulty script otherwise. Unfortunately at least Chrome doesn’t do the same and I’m not sure any behavior is specced.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search