Error handling with async-await in Vue and Vuex

January 11, 2018
3 minutes read

Check out the a demo here:

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.

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

Normally we would write something like this

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

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

// 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.

// 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:

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

// 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 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 lang='sass'>
  .fade-enter-active, .fade-leave-active
    transition: opacity .5s;

  .fade-enter, .fade-leave-to
    opacity: 0;


  .error-wrapper
    position: absolute
    top: 0
    right: 0
    .error
      padding: 0.5em 2em
      margin-top: 1em

      border-radius: 8px

      background: #cc0000
      color: #fff

</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.

// 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>
Thanks for reading!  ❤️
Due to privacy concerns, I've decided to remove Disqus.
Until I settle for another commenting solution, you can share your thoughts with me via e-mail
Back to Posts