iosvuejs2mobile-safarivue-routermobile-chrome

What causes this vue-router async component error - TypeError: undefined is not an object (evaluating 't.__esModule')?


I notice a lot of errors in Sentry with the following stack trace:

TypeError: undefined is not an object (evaluating 't.__esModule')
  at isESModule(./node_modules/vue-router/dist/vue-router.esm.js:1955:3)
  at ? (./node_modules/vue-router/dist/vue-router.esm.js:1882:27)
  at promiseReactionJob([native code])

I'm having a lot of trouble reproducing the error myself and figuring out what causes it. Looking at the vue-router source, it comes from this function:

  function isESModule (obj) {
    return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
  }

So obj is undefined. If we go one level up we get this function:

  function resolveAsyncComponents (matched) {
    return function (to, from, next) {
      var hasAsync = false;
      var pending = 0;
      var error = null;

      flatMapComponents(matched, function (def, _, match, key) {
        // if it's a function and doesn't have cid attached,
        // assume it's an async component resolve function.
        // we are not using Vue's default async resolving mechanism because
        // we want to halt the navigation until the incoming component has been
        // resolved.
        if (typeof def === 'function' && def.cid === undefined) {
          hasAsync = true;
          pending++;

          var resolve = once(function (resolvedDef) {
            if (isESModule(resolvedDef)) {
              resolvedDef = resolvedDef.default;
            }
            // save resolved on async factory in case it's used elsewhere
            def.resolved = typeof resolvedDef === 'function'
              ? resolvedDef
              : _Vue.extend(resolvedDef);
            match.components[key] = resolvedDef;
            pending--;
            if (pending <= 0) {
              next();
            }
          });

...

So it looks like resolvedDef is undefined.

My guess is that the Vue router resolves an async component successfully but it ends up undefined and there's nothing in the code to account for that case.

I'm using vue-router 3.1.3 and these errors always happen only on iOS (Safari or Chrome).

I've tried Googling the error and I can't seem to find a single reference to it anywhere else. I'm also not able to post an issue on the vue-router Github because I'm not able to provide a minimal reproduction.

Often (but not always) Sentry also shows this console log preceding the error:

console [vue-analytics] An error occured! Please check your connection or disable your AD blocker
logger console
extra {"arguments":["[vue-analytics] An error occured! Please check your connection or disable your AD blocker"]}

I'm not sure if that's related though.

One crazy solution I'm thinking of is deployed a patched version of vue-router where it throws an error with more useful context in the case where resolvedDef is undefined. That error would hopefully end up in our Sentry logs.

What's causing this error?


Solution

  • I forked the vue-router library and patched resolveAsyncComponents with this line:

    if (!resolvedDef) {
      window.onerror(`router wtf ${matched.join(',')} ${def} ${_} ${match} ${key} ${pending} ${hasAsync} ${error}`);
    }
    

    Eventually the following error appeared in Sentry:

    router wtf [object Object] function(){return u(Promise.all([n.e("dashboard~orders~products~publicUser"),n.e("dashboard~discounts~products~publicUser"),n.e("orders~publicUser~tips"),n.e("publicUser~tips"),n.e("publicUser")]).then(n.bind(null,"89c6")))...
    

    In production, our Vue app seems to load these components with an initiator called bootstrap:

    asycn component loading

    Then it hit me that we use the following wrapper around every single async component import in router.js:

          {
            path: 'dashboard',
            name: 'dashboard',
            component: () =>
              handleChunkLoadError(
                import(/* webpackChunkName: 'dashboard' */ '../views/Dashboard')
              ),
          },
    
    function handleChunkLoadError(importPromise) {
      return importPromise.catch(() => {
        Vue.toasted.error('Network error encountered', {
          duration: null,
          action: {
            text: 'Reload',
            onClick: () => window.location.reload(),
          },
        });
      });
    }
    

    It's supposed to show a toast that allows the user to refresh the page if a chunk failed to load.

    But we were forgetting to pass along the error to vue-router:

    function handleChunkLoadError(importPromise) {
      return importPromise.catch(error => {
        Vue.toasted.error('Network error encountered', {
          duration: null,
          action: {
            text: 'Reload',
            onClick: () => window.location.reload(),
          },
        });
    
        return error;
      });
    }
    

    Returning the error in the .catch handler fixes the issue.

    🤦