Uncaught (in promise) undefined - Vue-router

Dally picture Dally · Aug 14, 2019 · Viewed 23.2k times · Source

I'm facing a really strange issue where if a user with restricted permissions tries logging into my web app, they see the following error:

Uncaught (in promise) undefined

But this doesn't happen with users who have max permissions.

I think the issue is being caused by the re-reoute. If the user does not have page_access 1, it then routes to /holidays. The other strange thing is that this error only ever appears the once, and that's when the user first logs in. If the page is refreshed or the user navigates away to other pages, it does not appear.

router.js

Vue.use(Router)

const router = new Router({

  routes: [
    {
      path: '/',
      name: 'dashboard',
      component: Dashboard,
      beforeEnter(to, from, next) {
        if(localStorage.token) {
          if(localStorage.page_access.indexOf('1') != -1 && localStorage.page_access != null) {
            next('/holidays');
          }
          else {
            next();
          }
        } else {
          next('/login');
        }
      }
    },
    {
      path: '/holidays',
      name: 'holidays',
      component: Holidays,
      beforeEnter(to, from, next) {
        if(localStorage.token) {
          next();
        } else {
          next('/login');
        }
      }
    },
  ],
  mode: 'history'
})

router.beforeResolve((to, from, next) => {

  if(localStorage.token && from.name != 'login' && to.name != 'login') {
    store.dispatch('autoLogin')
    .then(response => {
      store.dispatch('getNavigation');
      next();
    })
    .catch(err => {
      console.log(err);
    });
  }
  else if(from.name && !localStorage.token) {
    router.go('/login');
  }
  else {
    next();
  }
});

export default router;

store.js

async autoLogin({commit}) {
    const token = localStorage.getItem('token');
    const remember_token = localStorage.getItem('remember_token');

    if(!token) {
      return;
    }

    try {
      const res = await axios({
      method: 'post',
      data: { userId: localStorage.user_id, token: localStorage.remember_token },
      url: 'https://controlapi.totalprocessing.com/api/get-user',
      config: { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
      })
      .then(response => {
        if(response.data.remember_token == remember_token) {
          commit('authUser', { token: token });
          return response;
        }
        else {
          localStorage.clear();
          return null;
        }
      })
      .catch(e => {
          this.errors.push(e);
          return e;
      })
      return res;
    }
    catch(e) {
      console.log(e);
      return e;
    }
}
getNavigation({commit}) {
    let pageAccess = localStorage.page_access == 'null' ? null : localStorage.page_access;
    let subPageAccess = localStorage.sub_page_access == 'null' ? null : localStorage.sub_page_access;

    axios({
    method: 'post',
    data: { pageAccess: pageAccess, subPageAccess: subPageAccess },
    url: 'https://controlapi.totalprocessing.com/api/client-get-navigation',
    config: { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
    })
    .then(response => {
    console.log(response.data);
        const data = response.data;
        const tree = [];

        data.reduce(function(a, b, i, r) {

            // Add the parent nodes
            if(a.page_id != b.page_id){
                tree.push({ page_id: a.page_id,
                            page_name: a.page_name,
                            page_path: a.path,
                            page_icon: a.page_icon
                            });
            }

            // Add the last parent node
            if(i+1 == data.length) {
                tree.push({ page_id: b.page_id,
                            page_name: b.page_name,
                            page_path: b.path,
                            page_icon: b.page_icon
                            });

                // Add the child nodes to the parent nodes
                data.reduce(function(a, b) {
                    if(a.sub_page_id) {
                        const find = tree.findIndex(f => f.page_id == a.parent_id);

                        // Add the first child node to parent
                        if(!("children" in tree[find])) {
                            tree[find].children = [];

                            tree[find].children.push({ page_id: a.sub_page_id,
                                                    page_name: a.sub_page_name,
                                                    page_path: a.sub_page_path,
                                                    page_icon: a.sub_page_icon
                            });
                        }
                        // Add the remaining child nodes to parent nodes
                        else {
                            tree[find].children.push({ page_id: a.sub_page_id,
                                                    page_name: a.sub_page_name,
                                                    page_path: a.sub_page_path,
                                                    page_icon: a.sub_page_icon
                            });
                        }
                    }
                    return b;
                });
            }
            return b;
        });

        commit('authNav', {
        navigation: tree
        });
    })
    .catch(e => {
        this.errors.push(e)
    })
}

Answer

agm1984 picture agm1984 · Dec 2, 2019

Based on my experience over the past few days, it is critical to catch errors in the function that calls this.$router.push().

I find two ways are immediately quite viable:

handleSomething() {
    this.$router.push({}).catch((err) => {
        throw new Error(`Problem handling something: ${err}.`);
    });
},

and

async handleSomething() {
    try {
        await this.$router.push({});
    } catch (err) {
        throw new Error(`Problem handling something: ${err}.`);    
    }
},

At the moment, I prefer against the async/await technique here because of its execution-blocking nature, but the key observation you should make is that the "uncaught in promise" error itself is a known issue in JavaScript often referred to as "a promise being swallowed", and it's caused by a Promise being rejected but that "error" is swallowed because it isn't properly caught. That is to say there is no block of code that catches the error, so your app cannot do anything in response to the error.

This means it is paramount to not swallow the error, which means you need to catch it somewhere. In my two examples, you can see the error will pass through the catch blocks.

Secondary to the error swallowing, is the fact that the error is even thrown to begin with. In my application where I saw this, it was hard to debug, but I can see the nature of the error has something to do with Vue components unloading and loading as the route changes. For example, if you call this.$router.push() in a component and then that component gets destroyed while the route-change is in-progress, it is reasonable that you could see an error like this.

As an extension of this problem, if a route-change occurs and the resultant component tries to read data from the .push() event before the Promise is resolved, it could also throw this error. The await should stop an error like that by instructing your application to wait before reading.

So in short, investigate those two things:

  1. are you somehow destroying/creating components while this.$router.push() is executing?
  2. is the next component possibly reading data about route parameters before the route-change is completed?

If you discover some of this could be happening, consider your data flow and make sure you solve it by taming the async behaviour, not just by suppressing the error. In my opinion, the error is a symptom of something bigger.

During your debugging, add console.log()s into all your components' created/mounted and destroyed lifecycle methods, and also into the functions related to the route change. You should be able to get a grasp on the way data is flowing.

I suspect the nature of this issue stems from downstream usage of this.$route.params during an in-flight route-change. Add lots of console.logs and/or step through a debugger.