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.
const requestUsers = () => fetch('https://jsonplaceholder.typicode.com/users');
Normally we would add a try-catch block around the call to handle any error.
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.
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.
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..
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.
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
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
<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.
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,
};
<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.