January 11, 2018

Error handling in Vue 2 and Vuex

Handling errors gracefully using async & await
In this article we're using Vue 2. I can’t guarantee that what's referred here is considered to be a good practice anymore, as I’m mostly involved with React these days.

Demo

I don't enjoy writing the error handling in JavaScript. I find it really annoying to have to wrap everything in a try-catch block. Especially for async calls. Let's see if we can make it a bit easier.

Let’s start by using a call that may fail.

index.js
const requestUsers = () => fetch('https://jsonplaceholder.typicode.com/users');

Normally we would add a try-catch block around the call to handle any error.

index.js
const doTheCall = async () => {
  try {
    const users = await requestUsers();
  } catch (e) {
    // something with the error
  }
};

Instead, let's create a wrapper function that will handle the error for us.

helpers.js
export const wrapRequest =
  (fn) =>
  (...params) =>
    fn(...params)
      .then((response) => {
        if (!response.ok) {
          throw response;
        }
        return response.json();
      })
      .catch((error) => handleError(error));

For this example let's write a simple function that will return the error message.

helpers.js
import store from './store';

const handleError = (error) => {
  const errorStatus = error ? error.status : error;
  const errorMessage = prepareErrorMessage(errorStatus);
  store.dispatch('populateErrors', errorMessage);
};

Vuex Setup

Let's set up a Vuex store..

store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

import errors from './_errors.js';
import users from './_users.js';
import loader from './_loader.js';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    errors,
    users,
    loader,
  },
});

And a Users module to handle the users list.

store/_users.js
import {wrappedRequestUsers} from '../requests';

const state = {
  usersList: [],
};
const getters = {
  usersList: (state) => state.usersList.length,
};
const mutations = {
  usersListSet: (state, list) => (state.usersList = list),
  updateLoader: (state, status) => (state.loading = status),
};
const actions = {
  requestUsers: async ({commit}) => {
    const data = await wrappedRequestUsers();
    if (data) commit('usersListSet', data);
  },
  clearUsersList: ({commit}) => {
    commit('usersListSet', []);
  },
};

export default {
  state,
  getters,
  mutations,
  actions,
};

As for the error handling actions, we will push the new error message in the state

store/_errors.js
const state = {
  errors: [],
};

const getters = {
  errors: (state) => state.errors,
};

const mutations = {
  addError: (state, error) => state.errors.unshift(error),
  popError: (state) => state.errors.pop(),
};

const actions = {
  populateErrors: ({commit}, error) => {
    commit('addError', error);
    setTimeout(() => commit('popError'), 3000);
  },
};

export default {
  state,
  getters,
  mutations,
  actions,
};

And the custom toast component will simply loop through every error message

components/errorToast.vue
<template>
  <div class="error-wrapper">
    <transition-group name="fade" tag="div">
      <div class="error" v-for="(error, index) in errors" :key="index">
        {{ error }}
      </div>
    </transition-group>
  </div>
</template>

<script>
import {mapGetters} from 'vuex';

export default {
  name: 'errorToast',
  computed: {
    ...mapGetters(['errors']),
  },
};
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.error-wrapper {
  position: absolute;
  top: 0;
  right: 0;
  .error {
    background: #cc0000;
    border-radius: 8px;
    color: #fff;
    margin-top: 1em;
    padding: 0.5em 2em;
  }
}
</style>

Use a spinner

Sometimes, we have to display a spinner. For this case, I’ve made a separate module for the loading instance. When a loader is needed, we won’t call the action directly, but instead we’ll dispatch executeWithLoader with the action name as a param.

store/_loader.js
const state = {
  loading: 0,
};
const getters = {
  loading: (state) => state.loading > 0,
  loadingStatus: (state) => (state.loading > 0 ? 'Fetching stuff' : 'Ready'),
};
const mutations = {
  updateLoader: (state, loading) =>
    (state.loading = loading ? state.loading++ : state.loading--),
};
const actions = {
  executeWithLoader: async ({commit, dispatch}, fn) => {
    commit('updateLoader', true);
    await dispatch(fn, {root: true});
    commit('updateLoader', false);
  },
};

export default {
  state,
  getters,
  mutations,
  actions,
};
App.vue
<button
  @click='executeWithLoader("requestUsers")'
  :disabled="loading"
  class="button button--success"
>
  Fetch users
</button>

There you have it. We have a simple way to handle errors in our Vue app.