Error handling with async-await in Vue 2 and Vuex

Handling errors gracefully using async & await

January 11th 20183 minutes 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.

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

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

Subscribe to the Newsletter

Why not get my latest content content by email?

Prefer RSS? Here's the feed.

I don't like spam, so I won't send you any.
Unsubscribe at any time.