Error handling with async-await in Vue 2 and Vuex

Handling errors gracefully using async & await

2018-01-114 min read
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

Preface

Async - await is a killer feature introduced in ES7.

What I really don't like, is having to wrap everything in a try-catch block. The code seams really busy, and repeating it everytime for every async action is a no go.

Solution

Let's start by using a call that may or may not, resolve successfully.

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

Normally we would write something like this

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

Instead of that, we will wrap the request with another function which will handle any error.

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

What 'HandleError' can do practically depends totally on your needs.

For now, let's assume that we catch the error and generate the appropriate message for each case. We will dispatch an action that will populate some error messages and call it a day.

JavaScript
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

Our vuex setup will be the following:

JavaScript
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,
  },
});

Where the users module is as simple as that

JavaScript
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

JavaScript
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 toastr component will simply loop through every error message

components/errorToastr.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: 'errorToastr',
  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 though, we have to display a spinner. For this reason, 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.

JavaScript
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>