Vue axios обработка ошибок

Логотип Vue
Vue.js

  • Обучение

    • Документация

      • Руководство
      • API
      • Рекомендации
      • Примеры
      • Книга рецептов
    • Видео курсы

      • Vue Mastery

      • Vue School

  • Экосистема

    • Помощь

      • Форум
      • Чат
      • Митапы
    • Инструментарий

      • Инструменты разработчика
      • Vue CLI
      • Vue Loader
    • Официальные плагины

      • Vue Router
      • Vuex
      • Vue Server Renderer
    • Новости

      • Еженедельные новости
      • Roadmap
      • События
      • Twitter
      • Блог
      • Вакансии
      • Сообщество разработчиков
  • Команда
  • Ресурсы

    • Партнёры
    • Темы
    • Awesome Vue
    • Найти пакеты для Vue
  • Поддержать Vue

    • Единоразовые пожертвования
    • Повторяющиеся взносы
    • Магазин футболок
  • Переводы

    • English
    • 中文
    • 日本語
    • 한국어
    • Português
    • Français
    • Tiếng Việt
    • Español
    • Bahasa Indonesia

Эта документация для версий v2.x и ранее.
Для v3.x, документация на русском здесь.

Используем axios для доступа к API

Простой пример

Неоднократно при создании веб-приложения вам может понадобиться получать и отображать данные из API. Существует несколько способов сделать это, но наиболее популярным решением является использование axios, основанного на Promise HTTP-клиента.

В этом упражнении мы будем использовать CoinDesk API для отображения цен на Биткойн, обновляемых каждую минуту. Прежде всего, подключим axios с помощью npm, yarn или ссылки на CDN.

Существует множество вариантов, как мы можем запрашивать информацию из API, но прежде необходимо узнать, в каком виде предоставляются данные, чтобы понимать, как их отображать. Для этого сделаем запрос к конечной точке (endpoint) API и выведем результат. Как можно убедиться из документации API CoinDesk, для получения данных мы будем делать запрос на https://api.coindesk.com/v1/bpi/currentprice.json. Изначально необходимо создать свойство в data для хранения нашей информации, далее извлечём и сохраним данные, используя хук жизненного цикла mounted.

new Vue({
el: '#app',
data() {
return {
info: null
};
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response));
}
});
<div id="app">
{{ info }}
</div>

И вот что мы получаем:

Посмотрите Pen First Step Axios and Vue by Vue (@Vue) на CodePen.

Отлично! Мы получили какие-то данные. Но сейчас они выглядят достаточно грязно, так что давайте отобразим их должным образом и добавим обработку ошибок на случай, если что-то пойдёт не так, либо когда для получения информации потребовалось больше времени.

Пример из жизни: работа с данными

Отображение данных из API

Типичная ситуация, когда необходимая информация находится внутри ответа сервера и нам необходимо знать структуру данных для получения доступа. В нашем случае, нужная информация о цене находится в response.data.bpi. Если мы используем это, то запрос будет выглядеть следующим образом.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi));

Посмотрите Pen Second Step Axios and Vue by Vue (@Vue) на CodePen.

Такие данные намного проще отображать, так что мы можем сейчас обновить HTML-разметку для отображения только нужной информации из полученных данных. Создадим фильтр, чтобы десятичные значения всегда отображались как нужно.

<div id="app">
<h1>Bitcoin Price Index</h1>
<div
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>
</div>
filters: {
currencydecimal (value) {
return value.toFixed(2)
}
}

Посмотрите Pen Third Step Axios and Vue by Vue (@Vue) на CodePen.

Обработка ошибок

Бывают моменты, когда мы не получили необходимые данные из API. Может быть множество причин, из-за которых вызов axios мог закончиться неудачей, например:

  • API не был доступен.
  • Запрос был сделан неправильно.
  • API не предоставил данные в ожидаемом нами формате.

При выполнении этого запроса мы должны проверять такие обстоятельства и предоставлять информацию в каждом случае, чтобы мы знали, как справиться с этой проблемой. В вызове axios мы сделаем это, используя catch.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi))
.catch(error => console.log(error));

Так мы узнаем, если что-то пойдёт не так во время запроса к API. Но что если данные повреждены или API не был доступен? Сейчас пользователь просто ничего не увидит. Мы могли бы использовать индикатор загрузки для этого случая и сообщать пользователю, что не можем получить данные.

new Vue({
el: '#app',
data() {
return {
info: null,
loading: true,
errored: false
};
},
filters: {
currencydecimal(value) {
return value.toFixed(2);
}
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => {
this.info = response.data.bpi;
})
.catch(error => {
console.log(error);
this.errored = true;
})
.finally(() => (this.loading = false));
}
});
<div id="app">
<h1>Bitcoin Price Index</h1>

<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>

<section v-else>
<div v-if="loading">Loading...</div>

<div
v-else
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>

</section>
</div>

Вы можете нажимать на кнопку перезапуска (находится в правом нижнем углу во вкладке Result), чтобы увидеть индикатор загрузки во время получения данных из API.

Посмотрите Pen Fourth Step Axios and Vue by Vue (@Vue) на CodePen.

В дальнейшем код можно улучшить с помощью компонентов для различных секций и более детальными отчётами об ошибках. Это зависит от используемого API и сложности вашего приложения.

Альтернативы

Fetch API

Fetch API — мощный нативный API для создания запросов. Может вы слышали, что одно из преимуществ Fetch API в том, что не нужно загружать внешние зависимости для его использования, что является правдой! Однако… он ещё не полностью поддерживается браузерами, поэтому всё равно необходимо использовать полифил. Есть подводные камни при работе с его API, поэтому многие сейчас предпочитают axios. В будущем это может измениться.

Если вы заинтересовались Fetch API — существуют очень хорошие статьи, где объясняются тонкости его использования.

Итоги

Существует множество способов работы с Vue и axios, выходящие за рамки получения и отображения данных из API. Вы можете также взаимодействовать с бессерверными функциями (Serverless Functions), публикацией/редактированием/удалением через API, к которому вы имеете доступ, и т.д. Простая интеграция этих двух библиотек сделала axios очень распространённым выбором среди разработчиков, которым необходимо интегрировать HTTP-клиенты в их приложения.

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
    console.log(error.config);
  });

Using the validateStatus config option, you can define HTTP code(s) that should throw an error.

axios.get('/user/12345', {
  validateStatus: function (status) {
    return status < 500; // Resolve only if the status code is less than 500
  }
})

Using toJSON you get an object with more information about the HTTP error.

axios.get('/user/12345')
  .catch(function (error) {
    console.log(error.toJSON());
  });

I just use the catch. The same thing I was using before I switched to vuex. It’s probably the most universal and well documented solution and lets me continue to insert my errors into the html of the components like I was doing before. It also lets me continue to use my loading = true, loading = false html animation.

So I end up with 3 state properties, data, error, and loading. It seems to work for me. Your mileage may vary. I am also using vuex modules and namespacing but here is a simplified example without that

//somevuexstore.js

actions: {

fetchData(context) {

    axios
        .get("api/someendpoint")
        .then(response => {
            context.commit('loading')
            context.commit('organizations', response.data)

        }).catch(error => {
            console.log(error.response.data.message || error.message)
            context.commit('error', error)
        });
},

mutations: {
organizations(state, data) {
    return state.organization = data
},

error(state, data) {
    return state.error = data
},

loading(state) {
    return state.loading = false
},

state= {

organization: [],
error: '',
loading: true
}

Then in my component.vue it’s very similar to the way I was doing it before, just with the added computed properties.

computed: {
...mapState({
        getError: 'error',
        getLoading: 'loading',
        getAllOrg: 'organization',
}),
}

mounted() {
      this.$store.dispatch('fetchData')
}

And my html would be stuff like this.

<tr v-for="value in getAllOrg" :key="value.id">
   <td>{{ value.id }}</td>
   <td>{{ value.email }}</td>
   <td>{{ value.name }}</td>
   <td>{{ value.['created-at'] | formatDate }}</td>
</tr>

I insert the error messages where appropriate

<div v-if="getError" class="error">
   <p>{{ getError }}</p>
</div>

For loading animation I use vue spinners package inserted into html where appropriate.

<div v-if="getLoading" style="height:37px;">
    <p>
      <bar-loader class="custom-class" color="#c2c2c2" 
      getLoading="getLoading" 
      :width="130"></bar-loader>
   </p>

Логотип Vue
Vue.js

  • Обучение

    • Документация

      • Руководство
      • API
      • Рекомендации
      • Примеры
      • Книга рецептов
    • Видео курсы

      • Vue Mastery

      • Vue School

  • Экосистема

    • Помощь

      • Форум
      • Чат
      • Митапы
    • Инструментарий

      • Инструменты разработчика
      • Vue CLI
      • Vue Loader
    • Официальные плагины

      • Vue Router
      • Vuex
      • Vue Server Renderer
    • Новости

      • Еженедельные новости
      • Roadmap
      • События
      • Twitter
      • Блог
      • Вакансии
      • Сообщество разработчиков
  • Команда
  • Ресурсы

    • Партнёры
    • Темы
    • Awesome Vue
    • Найти пакеты для Vue
  • Поддержать Vue

    • Единоразовые пожертвования
    • Повторяющиеся взносы
    • Магазин футболок
  • Переводы

    • English
    • 中文
    • 日本語
    • 한국어
    • Português
    • Français
    • Tiếng Việt
    • Español
    • Bahasa Indonesia

Эта документация для версий v2.x и ранее.
Для v3.x, документация на русском здесь.

Используем axios для доступа к API

Простой пример

Неоднократно при создании веб-приложения вам может понадобиться получать и отображать данные из API. Существует несколько способов сделать это, но наиболее популярным решением является использование axios, основанного на Promise HTTP-клиента.

В этом упражнении мы будем использовать CoinDesk API для отображения цен на Биткойн, обновляемых каждую минуту. Прежде всего, подключим axios с помощью npm, yarn или ссылки на CDN.

Существует множество вариантов, как мы можем запрашивать информацию из API, но прежде необходимо узнать, в каком виде предоставляются данные, чтобы понимать, как их отображать. Для этого сделаем запрос к конечной точке (endpoint) API и выведем результат. Как можно убедиться из документации API CoinDesk, для получения данных мы будем делать запрос на https://api.coindesk.com/v1/bpi/currentprice.json. Изначально необходимо создать свойство в data для хранения нашей информации, далее извлечём и сохраним данные, используя хук жизненного цикла mounted.

new Vue({
el: '#app',
data() {
return {
info: null
};
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response));
}
});
<div id="app">
{{ info }}
</div>

И вот что мы получаем:

Посмотрите Pen First Step Axios and Vue by Vue (@Vue) на CodePen.

Отлично! Мы получили какие-то данные. Но сейчас они выглядят достаточно грязно, так что давайте отобразим их должным образом и добавим обработку ошибок на случай, если что-то пойдёт не так, либо когда для получения информации потребовалось больше времени.

Пример из жизни: работа с данными

Отображение данных из API

Типичная ситуация, когда необходимая информация находится внутри ответа сервера и нам необходимо знать структуру данных для получения доступа. В нашем случае, нужная информация о цене находится в response.data.bpi. Если мы используем это, то запрос будет выглядеть следующим образом.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi));

Посмотрите Pen Second Step Axios and Vue by Vue (@Vue) на CodePen.

Такие данные намного проще отображать, так что мы можем сейчас обновить HTML-разметку для отображения только нужной информации из полученных данных. Создадим фильтр, чтобы десятичные значения всегда отображались как нужно.

<div id="app">
<h1>Bitcoin Price Index</h1>
<div
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>
</div>
filters: {
currencydecimal (value) {
return value.toFixed(2)
}
}

Посмотрите Pen Third Step Axios and Vue by Vue (@Vue) на CodePen.

Обработка ошибок

Бывают моменты, когда мы не получили необходимые данные из API. Может быть множество причин, из-за которых вызов axios мог закончиться неудачей, например:

  • API не был доступен.
  • Запрос был сделан неправильно.
  • API не предоставил данные в ожидаемом нами формате.

При выполнении этого запроса мы должны проверять такие обстоятельства и предоставлять информацию в каждом случае, чтобы мы знали, как справиться с этой проблемой. В вызове axios мы сделаем это, используя catch.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi))
.catch(error => console.log(error));

Так мы узнаем, если что-то пойдёт не так во время запроса к API. Но что если данные повреждены или API не был доступен? Сейчас пользователь просто ничего не увидит. Мы могли бы использовать индикатор загрузки для этого случая и сообщать пользователю, что не можем получить данные.

new Vue({
el: '#app',
data() {
return {
info: null,
loading: true,
errored: false
};
},
filters: {
currencydecimal(value) {
return value.toFixed(2);
}
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => {
this.info = response.data.bpi;
})
.catch(error => {
console.log(error);
this.errored = true;
})
.finally(() => (this.loading = false));
}
});
<div id="app">
<h1>Bitcoin Price Index</h1>

<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>

<section v-else>
<div v-if="loading">Loading...</div>

<div
v-else
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>

</section>
</div>

Вы можете нажимать на кнопку перезапуска (находится в правом нижнем углу во вкладке Result), чтобы увидеть индикатор загрузки во время получения данных из API.

Посмотрите Pen Fourth Step Axios and Vue by Vue (@Vue) на CodePen.

В дальнейшем код можно улучшить с помощью компонентов для различных секций и более детальными отчётами об ошибках. Это зависит от используемого API и сложности вашего приложения.

Альтернативы

Fetch API

Fetch API — мощный нативный API для создания запросов. Может вы слышали, что одно из преимуществ Fetch API в том, что не нужно загружать внешние зависимости для его использования, что является правдой! Однако… он ещё не полностью поддерживается браузерами, поэтому всё равно необходимо использовать полифил. Есть подводные камни при работе с его API, поэтому многие сейчас предпочитают axios. В будущем это может измениться.

Если вы заинтересовались Fetch API — существуют очень хорошие статьи, где объясняются тонкости его использования.

Итоги

Существует множество способов работы с Vue и axios, выходящие за рамки получения и отображения данных из API. Вы можете также взаимодействовать с бессерверными функциями (Serverless Functions), публикацией/редактированием/удалением через API, к которому вы имеете доступ, и т.д. Простая интеграция этих двух библиотек сделала axios очень распространённым выбором среди разработчиков, которым необходимо интегрировать HTTP-клиенты в их приложения.

I just use the catch. The same thing I was using before I switched to vuex. It’s probably the most universal and well documented solution and lets me continue to insert my errors into the html of the components like I was doing before. It also lets me continue to use my loading = true, loading = false html animation.

So I end up with 3 state properties, data, error, and loading. It seems to work for me. Your mileage may vary. I am also using vuex modules and namespacing but here is a simplified example without that

//somevuexstore.js

actions: {

fetchData(context) {

    axios
        .get("api/someendpoint")
        .then(response => {
            context.commit('loading')
            context.commit('organizations', response.data)

        }).catch(error => {
            console.log(error.response.data.message || error.message)
            context.commit('error', error)
        });
},

mutations: {
organizations(state, data) {
    return state.organization = data
},

error(state, data) {
    return state.error = data
},

loading(state) {
    return state.loading = false
},

state= {

organization: [],
error: '',
loading: true
}

Then in my component.vue it’s very similar to the way I was doing it before, just with the added computed properties.

computed: {
...mapState({
        getError: 'error',
        getLoading: 'loading',
        getAllOrg: 'organization',
}),
}

mounted() {
      this.$store.dispatch('fetchData')
}

And my html would be stuff like this.

<tr v-for="value in getAllOrg" :key="value.id">
   <td>{{ value.id }}</td>
   <td>{{ value.email }}</td>
   <td>{{ value.name }}</td>
   <td>{{ value.['created-at'] | formatDate }}</td>
</tr>

I insert the error messages where appropriate

<div v-if="getError" class="error">
   <p>{{ getError }}</p>
</div>

For loading animation I use vue spinners package inserted into html where appropriate.

<div v-if="getLoading" style="height:37px;">
    <p>
      <bar-loader class="custom-class" color="#c2c2c2" 
      getLoading="getLoading" 
      :width="130"></bar-loader>
   </p>

Error handling within an SPA can be challenging, most of the app uses some sort of backend service to store the app data, and things can go wrong due to network connection or a bad user Input. Working with Vue.js app I have figured that to handle all the API related errors best bet is to use axios inteceptor.

Typical error handling

Since axios is promise based HTTP library we can handle any error using then(response) and catch(error), but if you are not careful you will end up scattering catch() block throughout your application. In most cases, you will be showing some type of alert message to the screen.

axios.get('/user/1').then((response) => {
    console.log('Everything is awesome.');
}).catch((error) => {
    console.warn('Not good man :(');
})

It’s good for a small app but as your apps start growing it will be messy to maintain it, and if you decided to change the way you show errors to a user you will be doing a lot of work to update in all places.

Interceptor Error handling

Here comes the clean way of handling errors with axios interceptors. Idea is to check the error response and define some set of rules to show the errors on the global level. With this you don’t need the catch(error) block where you make axios call to handle request errors, all the error will be handled by interceptor.

Add following in resources/assets/js/bootstrap.js after axios import.

axios.interceptors.response.use(
function(response) { return response;}, 
function(error) {
    // handle error
    if (error.response) {
        alert(error.response.data.message);
    }
});

Error Notification

Just plain alert() box is not a great way to show errors, you cannot customize them and they look very ugly. I got this iziToast library which is pretty neat with lots of configuration and sweat animation backed in.

We can use it directly like this in our app but I prefer to create a wrapper around it so If some reason I need to change the notification system later I can just update the wrapper and everything will work with new hotter toast 2.0 notification in future 😎

Now pull the iziToast using npm and create a resources/assets/js/services/toast.js and add following code.

import 'izitoast/dist/css/iziToast.min.css'
import iZtoast from 'izitoast'

const toast = {
    error: (message, title = 'Error') => {
        return iZtoast.error({
            title: title,
            message: message,
            position: 'bottomCenter'
        });
    },
    success: (message, title = 'Success') => {
        return iZtoast.success({
            title: title,
            message: message,
            position: 'bottomCenter'
        });
    }
};

export default toast;

For simplicity, I am keeping only error and success type notification, but you can add all the features your app needed for notification.

Now let’s update the interceptor to use our toast notification instead boring alert box.

import toast from './toast'

axios.interceptors.response.use(
...
function(error) {
    // handle error
    if (error.response) {
        toast.error(error.response.data.message);
    }
});

Turn off error handling

I came across many scenarios where a global error handler was not a good choice. For example, if there is a request which error format is different or unique like currently, we are expecting message key in error response which is default in Laravel for any error. But if you are making calls to different API provider or you want to customize the notification, what to do then? Don’t worry axios configuration can help with this.

axios.get('/user/1', {errorHandle: false}).then((response) => {
    console.log('Everything is awesome.');
}).catch((error) => {
    // handle this error here
    alert('Not good man :(');
})

By passing a config property {errorHandle: false} in axios request we can turn off the error handling for this call. Now let’s modify the interceptor to make it happen.

axios.interceptors.response.use(
function (response) {
    return response;
}, 
function(error) {
    // check for errorHandle config
    if( error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false ) {
        return Promise.reject(error);
    }

    if (error.response) {
        toast.error(error.response.data.message);
    }
});

This makes it very flexible, now you can turn off global error handling for a specific endpoint call and you can use catch(error) to handle the error.

Error handler module

Since all the app needs this error handling let’s put it in a module so you can reuse it in any vuejs app or any app in general which uses axios to make HTTP calls.

Create resources/assets/js/services/errorHandler.js and add following code

import axios from 'axios'
import toast from './toast'

function errorResponseHandler(error) {
    // check for errorHandle config
    if( error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false ) {
        return Promise.reject(error);
    }

    // if has response show the error 
    if (error.response) {
        toast.error(error.response.data.message);
    }
}

// apply interceptor on response
axios.interceptors.response.use(
   response => response,
   errorResponseHandler
);

export default errorResponseHandler;

This module is applying the response interceptor to handle the errors during a request. To use it just import it in your resources/assets/js/bootstrap.js like this.

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

require('./services/errorHandler');

If you want to Display Form input error, do check out the vue directive which is based on a similar implementation.

I hope it gives you a clean way to handle the error in your API, let me know in the comments, see you next time.

Source Code

Introduction

I really love the problem/solution. approach. We see some problem, and then, a really nice solution. But for this talking, i think we need some introduction as well.

When you develop an web application, you generally want’s to separate the frontend and backend. Fo that, you need something that makes the communication between these guys.

To illustrate, you can build a frontend (commonly named as GUI or user interface) using vanilla HTML, CSS and Javascript, or, frequently, using several frameworks like Vue, React and so many more avaliable online. I marked Vue because it’s my personal preference.

Why? I really don’t study the others so deeply that i can’t assure to you that Vue is the best, but i liked the way he works, the syntax, and so on. It’s like your crush, it’s a personal choice.

But, beside that, any framework you use, you will face the same problem:_ How to communicate with you backend_ (that can be written in so many languages, that i will not dare mention some. My current crush? Python an Flask).

One solution is to use AJAX (What is AJAX? Asynchronous JavaScript And XML). You can use XMLHttpRequest directly, to make requests to backend and get the data you need, but the downside is that the code is verbose. You can use Fetch API that will make an abstraction on top of XMLHttpRequest, with a powerfull set of tools. Other great change is that Fetch API will use Promises, avoiding the callbacks from XMLHttpRequest (preventing the callback hell).

Alternatively, we have a awesome library named Axios, that have a nice API (for curiosity purposes, under the hood, uses XMLHttpRequest, giving a very wide browser support). The Axios API wraps the XMLHttpRequest into Promises, different from Fetch API. Beside that, nowadays Fetch API is well supported by the browsers engines available, and have polyfills for older browsers. I will not discuss which one is better because i really think is personal preference, like any other library or framework around. If you dont’t have an opinion, i suggest that you seek some comparisons and dive deep articles. Has a nice article that i will mention to you written by Faraz Kelhini.

My personal choice is Axios because have a nice API, has Response timeout, automatic JSON transformation, and Interceptors (we will use them in the proposal solution), and so much more. Nothing that cannot be accomplished by Fetch API, but has another approach.

The Problem

Talking about Axios, a simple GET HTTP request can be made with these lines of code:

import axios from 'axios'

//here we have an generic interface with basic structure of a api response:
interface HttpResponse<T> {
  data: T[]
}

// the user interface, that represents a user in the system
interface User {
  id: number
  email: string
  name: string
}

//the http call to Axios
axios.get<HttpResponse<User>>('/users').then((response) => {
  const userList = response.data
  console.log(userList)
})

Enter fullscreen mode

Exit fullscreen mode

We’ve used Typescript (interfaces, and generics), ES6 Modules, Promises, Axios and Arrow Functions. We will not touch them deeply, and will presume that you already know about them.

So, in the above code, if everything goes well, aka: the server is online, the network is working perfectly, so on, when you run this code you will see the list of users on console. The real life isn’t always perfect.

We, developers, have a mission:

Make the life of users simple!

So, when something is go bad, we need to use all the efforts in ours hands to resolve the problem ourselves, without the user even notice, and, when nothing more can be done, we have the obligation to show them a really nice message explaining what goes wrong, to easy theirs souls.

Axios like Fetch API uses Promises to handle asynchronous calls and avoid the callbacks that we mention before. Promises are a really nice API and not to difficult to understand. We can chain actions (then) and error handlers (catch) one after another, and the API will call them in order. If an Error occurs in the Promise, the nearest catch is found and executed.

So, the code above with basic error handler will become:

import axios from 'axios'

//..here go the types, equal above sample.

//here we call axios and passes generic get with HttpResponse<User>.
axios
  .get<HttpResponse<User>>('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  .catch((error) => {
    //try to fix the error or
    //notify the users about somenthing went wrong
    console.log(error.message)
  })

Enter fullscreen mode

Exit fullscreen mode

Ok, and what is the problem then? Well, we have a hundred errors that, in every API call, the solution/message is the same. For curiosity, Axios show us a little list of them: ERR_FR_TOO_MANY_REDIRECTS, ERR_BAD_OPTION_VALUE, ERR_BAD_OPTION, ERR_NETWORK, ERR_DEPRECATED, ERR_BAD_RESPONSE, ERR_BAD_REQUEST, ERR_CANCELED, ECONNABORTED, ETIMEDOUT. We have the HTTP Status Codes, where we found so many errors, like 404 (Page Not Found), and so on. You get the picture. We have too much common errors to elegantly handle in every API request.

The very ugly solution

One very ugly solution that we can think of, is to write one big ass function that we increment every new error we found. Besides the ugliness of this approach, it will work, if you and your team remember to call the function in every API request.

function httpErrorHandler(error) {
  if (error === null) throw new Error('Unrecoverable error!! Error is null!')
  if (axios.isAxiosError(error)) {
    //here we have a type guard check, error inside this if will be treated as AxiosError
    const response = error?.response
    const request = error?.request
    const config = error?.config //here we have access the config used to make the api call (we can make a retry using this conf)

    if (error.code === 'ERR_NETWORK') {
      console.log('connection problems..')
    } else if (error.code === 'ERR_CANCELED') {
      console.log('connection canceled..')
    }
    if (response) {
      //The request was made and the server responded with a status code that falls out of the range of 2xx the http status code mentioned above
      const statusCode = response?.status
      if (statusCode === 404) {
        console.log('The requested resource does not exist or has been deleted')
      } else if (statusCode === 401) {
        console.log('Please login to access this resource')
        //redirect user to login
      }
    } else if (request) {
      //The request was made but no response was received, `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in Node.js
    }
  }
  //Something happened in setting up the request and triggered an Error
  console.log(error.message)
}

Enter fullscreen mode

Exit fullscreen mode

With our magical badass function in place, we can use it like that:

import axios from 'axios'

axios
  .get('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  .catch(httpErrorHandler)

Enter fullscreen mode

Exit fullscreen mode

We have to remember to add this catch in every API call, and, for every new error that we can graciously handle, we need to increase our nasty httpErrorHandler with some more code and ugly if's.

Other problem we have with this approach, besides ugliness and lack of mantenability, is that, if in one, only single one API call, i desire to handle different from global approach, i cannot do.

The function will grow exponentially as the problems that came together. This solution will not scale right!

The elegant and recommended solution

When we work as a team, to make them remember the slickness of every piece of software is hard, very hard. Team members, come and go, and i do not know any documentation good enough to surpass this issue.

In other hand, if the code itself can handle these problems on a generic way, do-it! The developers cannot make mistakes if they need do nothing!

Before we jump into code (that is what we expect from this article), i have the need to speak some stuff to you understand what the codes do.

Axios allow we to use something called Interceptors that will be executed in every request you make. It’s a awesome way of checking permission, add some header that need to be present, like a token, and preprocess responses, reducing the amount of boilerplate code.

We have two types of Interceptors. Before (request) and After (response) an AJAX Call.

It’s use is simple as that:

//Intercept before request is made, usually used to add some header, like an auth
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    const token = window.localStorage.getItem('token') //do not store token on localstorage!!!
    config.headers.Authorization = token
    return config
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error)
  }
)

Enter fullscreen mode

Exit fullscreen mode

But, in this article, we will use the response interceptor, because is where we want to deal with errors. Nothing stops you to extend the solution to handle request errors as well.

An simple use of response interceptor, is to call ours big ugly function to handle all sort of errors.

As every form of automatic handler, we need a way to bypass this (disable), when we want. We are gonna extend the AxiosRequestConfig interface and add two optional options raw and silent. If raw is set to true, we are gonna do nothing. silent is there to mute notifications that we show when dealing with global errors.

declare module 'axios' {
  export interface AxiosRequestConfig {
    raw?: boolean
    silent?: boolean
  }
}

Enter fullscreen mode

Exit fullscreen mode

Next step is to create a Error class that we will throw every time we want to inform the error handler to assume the problem.

export class HttpError extends Error {
  constructor(message?: string) {
    super(message) // 'Error' breaks prototype chain here
    this.name = 'HttpError'
    Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
  }
}

Enter fullscreen mode

Exit fullscreen mode

Now, let’s write the interceptors:

// this interceptor is used to handle all success ajax request
// we use this to check if status code is 200 (success), if not, we throw an HttpError
// to our error handler take place.
function responseHandler(response: AxiosResponse<any>) {
  const config = response?.config
  if (config.raw) {
    return response
  }
  if (response.status == 200) {
    const data = response?.data
    if (!data) {
      throw new HttpError('API Error. No data!')
    }
    return data
  }
  throw new HttpError('API Error! Invalid status code!')
}

function responseErrorHandler(response) {
  const config = response?.config
  if (config.raw) {
    return response
  }
  // the code of this function was written in above section.
  return httpErrorHandler(response)
}

//Intercept after response, usually to deal with result data or handle ajax call errors
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.response.use(responseHandler, responseErrorHandler)

Enter fullscreen mode

Exit fullscreen mode

Well, we do not need to remember our magical badass function in every ajax call we made. And, we can disable when we want, just passing raw to request config.

import axios from 'axios'

// automagically handle error
axios
  .get('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  //.catch(httpErrorHandler) this is not needed anymore

// to disable this automatic error handler, pass raw
axios
  .get('/users', {raw: true})
  .then((response) => {
    const userList = response.data
    console.log(userList)
  }).catch(() {
    console.log("Manually handle error")
  })

Enter fullscreen mode

Exit fullscreen mode

Ok, this is a nice solution, but, this bad-ass ugly function will grow so much, that we cannot see the end. The function will become so big, that anyone will want to maintain.

Can we improve more? Oh yeahhh.

The IMPROVED and elegant solution

We are gonna develop an Registry class, using Registry Design Pattern. The class will allow you to register error handling by an key (we will deep dive in this in a moment) and a action, that can be an string (message), an object (that can do some nasty things) or an function, that will be executed when the error matches the key. The registry will have parent that can be placed to allow you override keys to custom handle scenarios.

Here are some types that we will use througth the code:

// this interface is the default response data from ours api
interface HttpData {
  code: string
  description?: string
  status: number
}

// this is all errrors allowed to receive
type THttpError = Error | AxiosError | null

// object that can be passed to our registy
interface ErrorHandlerObject {
  after?(error?: THttpError, options?: ErrorHandlerObject): void
  before?(error?: THttpError, options?: ErrorHandlerObject): void
  message?: string
  notify?: QNotifyOptions
}

//signature of error function that can be passed to ours registry
type ErrorHandlerFunction = (error?: THttpError) => ErrorHandlerObject | boolean | undefined

//type that our registry accepts
type ErrorHandler = ErrorHandlerFunction | ErrorHandlerObject | string

//interface for register many handlers once (object where key will be presented as search key for error handling
interface ErrorHandlerMany {
  [key: string]: ErrorHandler
}

// type guard to identify that is an ErrorHandlerObject
function isErrorHandlerObject(value: any): value is ErrorHandlerObject {
  if (typeof value === 'object') {
    return ['message', 'after', 'before', 'notify'].some((k) => k in value)
  }
  return false
}

Enter fullscreen mode

Exit fullscreen mode

So, with types done, let’s see the class implementation. We are gonna use an Map to store object/keys and a parent, that we will seek if the key is not found in the current class. If parent is null, the search will end. On construction, we can pass an parent,and optionally, an instance of ErrorHandlerMany, to register some handlers.

class ErrorHandlerRegistry {
private handlers = new Map<string, ErrorHandler>()
private parent: ErrorHandlerRegistry | null = null
constructor(parent: ErrorHandlerRegistry = undefined, input?: ErrorHandlerMany) {
if (typeof parent !== 'undefined') this.parent = parent
if (typeof input !== 'undefined') this.registerMany(input)
}
// allow to register an handler
register(key: string, handler: ErrorHandler) {
this.handlers.set(key, handler)
return this
}
// unregister a handler
unregister(key: string) {
this.handlers.delete(key)
return this
}
// search a valid handler by key
find(seek: string): ErrorHandler | undefined {
const handler = this.handlers.get(seek)
if (handler) return handler
return this.parent?.find(seek)
}
// pass an object and register all keys/value pairs as handler.
registerMany(input: ErrorHandlerMany) {
for (const [key, value] of Object.entries(input)) {
this.register(key, value)
}
return this
}
// handle error seeking for key
handleError(
this: ErrorHandlerRegistry,
seek: (string | undefined)[] | string,
error: THttpError
): boolean {
if (Array.isArray(seek)) {
return seek.some((key) => {
if (key !== undefined) return this.handleError(String(key), error)
})
}
const handler = this.find(String(seek))
if (!handler) {
return false
} else if (typeof handler === 'string') {
return this.handleErrorObject(error, { message: handler })
} else if (typeof handler === 'function') {
const result = handler(error)
if (isErrorHandlerObject(result)) return this.handleErrorObject(error, result)
return !!result
} else if (isErrorHandlerObject(handler)) {
return this.handleErrorObject(error, handler)
}
return false
}
// if the error is an ErrorHandlerObject, handle here
handleErrorObject(error: THttpError, options: ErrorHandlerObject = {}) {
options?.before?.(error, options)
showToastError(options.message ?? 'Unknown Error!!', options, 'error')
return true
}
// this is the function that will be registered in interceptor.
resposeErrorHandler(this: ErrorHandlerRegistry, error: THttpError, direct?: boolean) {
if (error === null) throw new Error('Unrecoverrable error!! Error is null!')
if (axios.isAxiosError(error)) {
const response = error?.response
const config = error?.config
const data = response?.data as HttpData
if (!direct && config?.raw) throw error
const seekers = [
data?.code,
error.code,
error?.name,
String(data?.status),
String(response?.status),
]
const result = this.handleError(seekers, error)
if (!result) {
if (data?.code && data?.description) {
return this.handleErrorObject(error, {
message: data?.description,
})
}
}
} else if (error instanceof Error) {
return this.handleError(error.name, error)
}
//if nothings works, throw away
throw error
}
}
// create ours globalHandlers object
const globalHandlers = new ErrorHandlerRegistry()

Enter fullscreen mode

Exit fullscreen mode

Let’s deep dive the resposeErrorHandler code. We choose to use key as an identifier to select the best handler for error. When you look at the code, you see that has an order that key will be searched in the registry. The rule is, search for the most specific to the most generic.

const seekers = [
data?.code, //Our api can send an error code to you personalize the error messsage.
error.code, //The AxiosError has an error code too (ERR_BAD_REQUEST is one).
error?.name, //Error has a name (class name). Example: HttpError, etc..
String(data?.status), //Our api can send an status code as well.
String(response?.status), //respose status code. Both based on Http Status codes.
]

Enter fullscreen mode

Exit fullscreen mode

This is an example of an error sent by API:

{
"code": "email_required",
"description": "An e-mail is required",
"error": true,
"errors": [],
"status": 400
}

Enter fullscreen mode

Exit fullscreen mode

Other example, as well:

{
"code": "no_input_data",
"description": "You doesnt fill input fields!",
"error": true,
"errors": [],
"status": 400
}

Enter fullscreen mode

Exit fullscreen mode

So, as an example, we can now register ours generic error handling:

globalHandlers.registerMany({
//this key is sent by api when login is required
login_required: {
message: 'Login required!',
//the after function will be called when the message hides.
after: () => console.log('redirect user to /login'),
},
no_input_data: 'You must fill form values here!',
//this key is sent by api on login error.
invalid_login: {
message: 'Invalid credentials!',
},
'404': { message: 'API Page Not Found!' },
ERR_FR_TOO_MANY_REDIRECTS: 'Too many redirects.',
})
// you can registre only one:
globalHandlers.register('HttpError', (error) => {
//send email to developer that api return an 500 server internal console.error
return { message: 'Internal server errror! We already notify developers!' }
//when we return an valid ErrorHandlerObject, will be processed as whell.
//this allow we to perform custom behavior like sending email and default one,
//like showing an message to user.
})

Enter fullscreen mode

Exit fullscreen mode

We can register error handler in any place we like, group the most generic in one typescript file, and specific ones inline. You choose. But, to this work, we need to attach to ours http axios instance. This is done like this:

function createHttpInstance() {
const instance = axios.create({})
const responseError = (error: any) => globalHandlers.resposeErrorHandler(error)
instance.interceptors.response.use(responseHandler, responseError)
return instance
}
export const http: AxiosInstance = createHttpInstance()

Enter fullscreen mode

Exit fullscreen mode

Now, we can make ajax requests, and the error handler will work as expected:

import http from '/src/modules/http'
// automagically handle error
http.get('/path/that/dont/exist').then((response) => {
const userList = response.data
console.log(userList)
})

Enter fullscreen mode

Exit fullscreen mode

The code above will show a Notify ballon on the user screen, because will fire the 404 error status code, that we registered before.

Customize for one http call

The solution doesn’t end here. Let’s assume that, in one, only one http request, you want to handle 404 differently, but just 404. For that, we create the dealsWith function below:

export function dealWith(solutions: ErrorHandlerMany, ignoreGlobal?: boolean) {
let global
if (ignoreGlobal === false) global = globalHandlers
const localHandlers = new ErrorHandlerRegistry(global, solutions)
return (error: any) => localHandlers.resposeErrorHandler(error, true)
}

Enter fullscreen mode

Exit fullscreen mode

This function uses the ErrorHandlerRegistry parent to personalize one key, but for all others, use the global handlers (if you wanted that, ignoreGlobal is there to force not).

So, we can write code like this:

import http from '/src/modules/http'
// this call will show the message 'API Page Not Found!'
http.get('/path/that/dont/exist')
// this will show custom message: 'Custom 404 handler for this call only'
// the raw is necessary because we need to turn off the global handler.
http.get('/path/that/dont/exist', { raw: true }).catch(
dealsWith({
404: { message: 'Custom 404 handler for this call only' },
})
)
// we can turn off global, and handle ourselves
// if is not the error we want, let the global error take place.
http
.get('/path/that/dont/exist', { raw: true })
.catch((e) => {
//custom code handling
if (e.name == 'CustomErrorClass') {
console.log('go to somewhere')
} else {
throw e
}
})
.catch(
dealsWith({
404: { message: 'Custom 404 handler for this call only' },
})
)

Enter fullscreen mode

Exit fullscreen mode

The Final Thoughts

All this explanation is nice, but code, ah, the code, is so much better. So, i’ve created an github repository with all code from this article organized to you try out, improve and customize.

  • Click here to access the repo in github.

FOOTNOTES:

  • This post became so much bigger than a first realize, but i love to share my thoughts.
  • If you have some improvement to the code, please let me know in the comments.
  • If you see something wrong, please, fix-me!

Introduction

Axios is a JavaScript library that uses the Promise API to create HTTP requests with http in Node.js runtime or XMLHttpRequests in the browser. Because these requests are promises, they work with the newer async/await syntax, as well as .then() functions for promise chaining and the .catch() mechanism for error handling.

try {
let res = await axios.get('/my-api-route');
// Work with the response...
} catch (err) {
// Handle error
console.log(err);
}

In this article, we will see how to handle errors with Axios, as this is very important when making any HTTP calls knowing fully well that there are times when the service you’re calling might not be available or return other unexpected errors. We’ll show the .then()/.catch() method, but primarily use the async/await syntax.

Then and Catch

Promises can be handled in two ways using modern JS — the async/await syntax, which was shown above, as well as .then() and .catch() methods. Note that both of these methods can produce the same functionality, but async/await is typically regarded as being easier to work with and requires less boilerplate code in longer promise chains.

Here is how you’d achieve the same thing, but using the then/catch method:

axios.get('/my-api-route')
.then(res => {
// Work with the response...
}).catch(err => {
// Handle error
console.log(err);
});

Both the res and err objects are the same as with the async/await syntax.

Handling Errors

In this section, we will look at two primary categories of problems, as well as other issues that we may encounter and how to manage them using Axios. It is critical that you understand that this applies to all types of HTTP queries handled by Axios, including GET, POST, PATCH, and so on.

Here you can see the syntax for the three aspects — this will capture the error; it is crucial to note that this error carries a large error object with a lot of information:

try {
let res = await axios.get('/my-api-route');
// Work with the response...
} catch (err) {
if (err.response) {
// The client was given an error response (5xx, 4xx)
} else if (err.request) {
// The client never received a response, and the request was never left
} else {
// Anything else
}
}

The differences in the error object, highlighted above in the catch code, indicate where the request encountered the issue. We’ll look deeper into this in the following sections.

error.response

This is the type of mistake we are most familiar with, and it is much easier to deal with. Many sites display a 404 Not Found page/error message or various response codes based on what the API provides; this is often handled via the response.

If your error object has a response property, it signifies your server returned a 4xx/5xx error. This will assist you choose what sort of message to return to users; the message you’ll want to provide for 4xx may differ from that for 5xx, and if your backend isn’t returning anything at all.

try {
let res = await axios.get('/my-api-route');
// Work with the response...
} catch (err) {
if (err.response) {
// The client was given an error response (5xx, 4xx)
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
} else if (err.request) {
// The client never received a response, and the request was never left
} else {
// Anything else
}
}

error.request

This error is most commonly caused by a bad/spotty network, a hanging backend that does not respond instantly to each request, unauthorized or cross-domain requests, and lastly if the backend API returns an error.

Note: This occurs when the browser was able to initiate a request but did not receive a valid answer for any reason.

try {
let res = await axios.get('/my-api-route');
// Work with the response...
} catch (err) {
if (err.response) {
// The client was given an error response (5xx, 4xx)
} else if (err.request) {
// The client never received a response, and the request was never left
console.log(err.request);
} else {
// Anything else
}
}

Earlier we mentioned that the underlying request Axios uses depends on the environment in which it’s being run. This is also the case for the err.request object. Here the err.request object is an instance of XMLHttpRequest when being executed in the browser, whereas it’s an instance of http.ClientRequest when being used in Node.js.

Other Errors

It’s possible that the error object does not have either a response or request object attached to it. In this case it is implied that there was an issue in setting up the request, which eventually triggered an error.

try {
let res = await axios.get('/my-api-route');
// Work with the response...
} catch (err) {
if (err.response) {
// The client was given an error response (5xx, 4xx)
} else if (err.request) {
// The client never received a response, and the request was never left
} else {
// Anything else
console.log('Error', err.message);
}
}

For example, this could be the case if you omit the URL parameter from the .get() call, and thus no request was ever made.

Conclusion

In this short article, we looked at how we may handle various sorts of failures and errors in Axios. This is also important for giving the correct message to your application/website visitors, rather than always returning a generic error message, sending a 404, or indicating network problems.


сентябрь
17
, 2019

Когда мы создаем любые приложения, не стоит рассчитывать, что пользователи будут работать с ним так, как мы задумали. Нет, они постоянно будут делать тупые с нашей точки зрения вещи:

— добавлять товары с пустыми названиями

— прописывать цены строками

— копипастить портянки текста в описания, хотя мы четко написали в инструкции — не больше 200 символов.

Работать с нашим приложением будут не программисты. Представьте менеджера, которому нужно забить в базу 3 сотни товаров, и это до завтра.
Он не будет изучать нашу инструкцию, он сядет и сразу будет набивать по списку. Так сделает любой нормальный человек.

Наша задача, как программистов, сделать так, чтобы система обрабатывала любые данные.
Приложение должно или распарсить эти данные и понять, что хотел ввести пользователь, или приостановить работу и сказать, в чем ошибка.
И сказать человеческим языком, а не ошибкой в консоли. Сегодня мы займемся этой проблемой и посмотрим, как адекватно обрабатывать разные ошибки.

Валидация на клиенте и сервере. В чем разница?

Валидация на клиенте — это те ошибки, которые мы можем проверить средствами браузера. Например,

— юзер пытается добавить товар с пустым названием

— юзер забыл указать цену товара

— юзер вбивает длинное описание, хотя допускается максимум 200 символов.

Все это можно проверить в клиентской части приложения с помощью javascript.

Валидация на сервере — это все то, что можно проверить на клиенте, плюс некоторые специфичные вещи, часто связанные с базой. Например,

— юзер пытается добавить уже существующий товар

— юзер пытается удалить категорию, хотя в ней есть какие-то товары

— юзер пытается удалить бренд, но у него нет прав доступа на это

Все эти вещи проверяются на бекенде, клиент о них не знает.

Отмечу 2 момента:

— стоит по максимуму валидировать данные на клиенте

— дублировать эти же проверки на сервере.

Зачем это нужно?

Валидация на клиенте выполняется быстро и не требует лишнего запроса на сервер. Это ускоряет работу приложения.
Действительно, зачем юзеру ждать, пока запрос сбегает на сервер, если нужно всего лишь проверить, не пустое ли поле названия товара?

Дублировать эти же проверки на сервере нужно для других случаев. Во-первых, нашим приложением могут пользоваться по API, то есть не через интерфейс, а дергать rest-запросы напрямую.
И тут валидация на клиенте не поможет. А во-вторых, могут найтись какие-нибудь хакеры или бродящие по интернету боты, которые захотят поломать наш сервис, закинув в него неподходящие данные.

Вырисовывается простое правило: валидировать на клиенте нужно для нормальных людей, а дублировать те же проверки на сервере — для нехороших ботов и взломщиков.

Валидация в нашей админке интернет-магазина

До какой степени валидировать данные в приложении, каждый решает сам. Процесс этот бесконечный, можно проверять все, что угодно.
Главное, вовремя остановиться и не забыть проверить самые важные вещи.

В нашей админке мы не будем стелить соломку по всему коду, смысл уроков не в этом. Мы подробно рассмотрим валидацию только в одном компоненте, но покроем и клиентские, и серверные проверки.
И научимся показывать юзерам ошибки в человеческом виде.

Мы проработаем форму добавления брендов, которую сделали в прошлом уроке.
Сейчас мы можем добавить любой бренд, потому что никаких проверок нет. Исправим это и будем проверять такие вещи:

— не пустой ли бренд

— не превышает ли бренд 20 символов (практического смысла мало, но для примера нормально)

— существует ли этот бренд в базе

— нет ли ошибки в запросе (неверный роутер, проходили в третьем уроке)

— общие ошибки, например, сервис недоступен, бекенд лежит

Первые 2 проверки будем делать на клиенте, следующие 2 — на сервере. Пятая — это все невошедшие ошибки, например, упавший ajax-запрос или 500-ка от сервера.

Дублировать клиентские проверки на сервере мы не будем. Они очень простые и легко реализуются на php, не будем перегружать код.
Другие серверные ошибки, номер 3 и 4, уже реализованы в третьем уроке, когда мы готовили api для админки.

Общая схема обработки ошибок

Сделаем так. В окошке добавления бренда заведем поле, куда будем выводить сообщение об ошибке.
Пользователям все равно, клиентская это ошибка или серверная, для него это будет выглядеть одинаково, вот так

Чтобы понимать, какая именно ошибка произошла, добавим такое понятие — код ошибки. Например, на скриншоте код «brand_exists», что означает, бренд уже существует.
Это проверка серверная, но красное сообщение об этом не знает.
Код ошибки — единственное, на что будет ориентироваться это сообщение и все равно, откуда он придет, при клиентской валидации или с ответом от сервера.
Тексты сообщений мы будем брать из отдельного конфига. Теперь давайте сделаем это на практике.

Валидация на клиенте

Откроем компонент BrandsNew.vue и добавим новое поле errorCode, в раздел data. Это тот самый код, который определяет наличие ошибки. По умолчанию — пустая строка. То есть вот так

    data () {
return {
visible: false,
newBrand: '',
// Новое поле
errorCode: ''
}
}

Добавим еще вычисляемое логическое поле isError, которое покажет, если ли в данный момент ошибка

    computed: {
isError () {
return this.errorCode !== '';
}
}

Дальше нам нужно вывести сообщение об ошибки в окне добавления бренда. Добавим такой код между инпутом-брендом и кнопкой Добавить

    Ошибка: {{errorCode}}

Это выделенный красным текст, пример взяли из библиотеки minicss. Напоминаю, наше приложение на этом css-фреймворке. Чуток поправим стили, добавим в style

    mark {
display: block;
margin: 5px 0 10px 5px;
}

Но вернемся к разметке. v-if=»isError» означает, что сообщение будем выводить только, когда есть ошибка, логично. А в тексте сообщения выведем пока код ошибки — {{errorCode}}.
Сначала убедимся, что наша схема работает, а потом заменим бездушный errorCode нормальным текстом.

Итак, errorCode у нас есть, нужны методы для работы с ним. Один метод будет устанавливать код, другой очищать. Добавим в раздел methods

    setError (errorCode) {
this.errorCode = errorCode;
},
clearError () {
this.errorCode = '';
}

Можно было обойтись и одним только setError, но позже увидим, что clearError удобная штука и нам еще пригодится.

Базовая подготовка сделана, пора заняться самой валидацией. Напомню, мы будем проверять, что новый бренд не пустой и что не длиннее 20 символов.
Метод валидации будет выполнять эти проверки и в случае ошибкок устанавливать соответствующий код codeError. Вот так выглядит метод validate

    validate (brand) {
if (brand === '') {
this.setError('brand_empty');
return false;
}
if (brand.length > 20) {
this.setError('brand_long_title');
return false;
}
// Если валидация прошла успешно
this.clearError();
return true;
}

Два простых if-а, которые проставляют нужные коды brand_empty или brand_long_title. А в случае успеха дергаем метод clearError, чтобы сбросить ошибку, если она была до этого.
Например, юзер ввел правильно инфу со второго раза.

Важный момент. Помимо установки codeError мы возвращаем true или false в зависимости от итога валидации.
Нам это нужно, чтобы понять, отправлять ли запрос на сервер с добавлением бренда или же ждать, пока юзер введет бренд правильно.

Посмотрим, какой метод добавления бренда мы написали раньше

    addBrand () {
this.$store.dispatch('brands/addBrand', this.newBrand);
this.newBrand = '';
this.closeModal();
}

Все просто, вызываем действие через dispatch, очищаем инпут с названием и закрываем модалку. Этот код нужно чуть переделать, вызывать действие только если валидация прошла успешно.
Добавим условие if

    addBrand () {
if (this.validate(this.newBrand)) {
this.$store.dispatch('brands/addBrand', this.newBrand);
this.newBrand = '';
this.closeModal();
}
}

На этом с клиентской валидацией все, можно проверять. Пробуем добавить пустой или чересчур длинный бренд, увидим сообщение с нужным кодом ошибки, а запрос на сервер не отправится.
Красота. Идем дальше.

Валидация на сервере

С серверными проверками есть свои особенности.

Во-первых, на клиенте мы генерили коды ошибок сами, а для серверных нужно дождаться ответа и получить их из респонса.
Да, мы эти коды (brand_exists и invalid_router) сами сделали в уроке по API. Но вообще мы можем даже и не знать, что за коды нам возвращает бекенд.

Во-вторых, есть интерфейсный момент. Сейчас в методе addBrand сразу после dispatch мы очищаем поле бренда и закрываем модалку.
Это годится, если бренд успешно добавлен и никуда не годится, если сервер вернул ошибку.
Значит, мы должны не просто дернуть dispatch, а дождаться ответа от сервера и только потом решать, что делать.
Если ответ 200 и бренд добавлен, то закрывать модалку, а если ошибка, то выводить ее и оставлять модалку.

Если бы мы писали приложение на jquery, то сделали бы примерно так

    $.ajax({
url: '',
data: '',
success: function() {
// очищаем поле бренда и закрываем модалку
},
error: function() {
// парсим код ошибки и выводим красное сообщение в модалке
}
});

Но мы уже серьезные ребята, у нас тут vue и вообще на хабре пишут, что ад из колбеков — это неправильно. Подключим промисы, здесь как раз тот случай, когда без них будет сложно.

Начнем с действия addBrand. Открываем файл store/modules/brands.js и смотрим, как реализовано сейчас

    addBrand (context, newBrand) {
const data = 'title=' + newBrand;
axios
.post('/admin/api/v1/brands', data)
.then(response => {
context.commit('ADD_BRAND', response.data)
});
}

Отправляем запрос на сервер через axios и в случае успеха вызываем мутацию ADD_BRAND. Нам нужно сделать то же самое, только при этом еще и возвращать промис. Вот так

    addBrand (context, newBrand) {
const data = 'title=' + newBrand;
return new Promise((resolve, reject) => {
axios
.post('/admin/api/v1/brands', data)
.then(response => {
context.commit('ADD_BRAND', response.data);
resolve(response);
}, error => {
reject(error);
});
});
}

Это стандартная заготовка для промисов. resolve и reject — это функции, которые выполняются соответственно при успехе и ошибке.
А что именно делают эти функции, будем решать уже в другом месте — опять в компоненте BrandsNew, в методе addBrand. Немного изменим код.

Было

    addBrand () {
if (this.validate(this.newBrand)) {
this.$store.dispatch('brands/addBrand', this.newBrand);
this.newBrand = '';
this.closeModal();
}
}

Стало

    addBrand () {
if (this.validate(this.newBrand)) {
this.$store.dispatch('brands/addBrand', this.newBrand).then(
response => {
this.newBrand = '';
this.closeModal();
},
error => {
let resp = error.response;
let errorCode = (resp && resp.data && resp.data.code) ? resp.data.code : 'unknown_error';
this.setError(errorCode);
});
}
}

Кода прибавилось, но немного. В метод resolve попадает

    response => {
this.newBrand = '';
this.closeModal();
}

Параметр response нам не нужен, просто для наглядности. Получается, если запрос прошел успешно, бренд добавлен, то очищаем поле в модалке и саму модалку закрываем.
При ошибке выполняется другое

    error => {
let resp = error.response;
let errorCode = (resp && resp.data && resp.data.code) ? resp.data.code : 'unknown_error';
this.setError(errorCode);
});

Здесь параметр error уже нужен — это ответ от сервера, который нужно распарсить и вытащить код ошибки. Разберем по порядку. В первой строке пишем в переменную resp ответ от сервера.
Дальше идет диковатая конструкция resp && resp.data && resp.data.code. Зачем так сложно?
Ведь мы возвращаем ответ с бекенда в виде { code: ‘код ошибки’, … }, то есть поле code там будет всегда. Не совсем.
Код будет, но только для тех случаев, когда наш бекенд нормально обработает ошибку. Например, найдет, что бренд уже существует, или невалидный роутер (ошибка в запросе, в урле).
Если же бекенд просто лежит и вернет нам 500-ку, то в error.response может прийти null или пустая строка. Поэтому resp.data.code выкинет нам ошибку в консоль и поломает работу.

Поэтому именно здесь мы подстилаем соломку и проверяем всю цепочку error.response.data.code.
Только если data.code действительно существует, то мы понимаем, что эту ошибку вернул наш бекенд и в errorCode мы записываем валидное значение.
А в противном случае мы понимаем, что случилась какая-то хрень и поэтому ставим unknown_error. Типа «случилась непонятная хрень». Так и напишем пользователю.

Задача программиста как можно меньше пугать юзеров непонятными ошибками и стараться по максимуму обрабатывать их руками.
Другое дело, что пользователям вообще по фигу, лежит ли у вас бекенд, упала ли база и кто-то накосячил в коде. Ему важно, что приложение не работает и все.
Поэтому есть смысл не перегружать юзера техническими деталями, а выдать общую ошибку и попросить попробовать позже. Естественно, предполагается, что мы уже устраняем косяки и поднимаем бекенд.

Это все, что касается валидации на сервере. Можно проверять, попытаться добавить существующий бренд или изменить урл /admin/api/v1/brands на какой-нибудь левый.
Если все сделали правильно, то при добавлении бренда будет уходить запрос на сервер, возвращаться ошибка и модалка не закроется, а выведет код.

Когда мы будем тестировать работу, то заметим одну визуальную неприятность. Допустим, попытались мы добавить пустой бренд, нам выскочила ошибка brand_empty.
Изменяем бренд в инпуте, а ошибка так и торчит. Бесит. Давайте добавим одну строчку, чтобы при установке фокуса в инпуте ошибка пропадала. Теперь инпут будет выглядеть так

    

@focus=»clearError» — вот и пригодился метод clearError!

Вывод нормального сообщения об ошибке вместо кода

Коды ошибок у нас есть, осталось задать им адекватные тексты. Можно сделать это и в компоненте BrandsNew, но мы поступим интереснее.
Заведем в папке admin/vue/src папку configs, а в ней файлик brands.js. Давно было пора, потому что хорошее дело — выносить подобные вещи в конфиги, а не держать их в компонентах.
В файлике-конфиге напишем так

    export default {
errors: [{
code: 'brand_empty',
message: 'Бренд не может быть пустым'
}, {
code: 'brand_long_title',
message: 'Название не должно превышать 20 символов'
}, {
code: 'brand_exists',
message: 'Бренд с таким названием уже существует'
}, {
code: 'invalid_router',
message: 'Ошибка запроса. Попробуйте еще раз'
}, {
code: 'unknown_error',
message: 'Неизвестная ошибка. Попробуйте позже'
}]
}

Просто массив из двух полей: код ошибки и текст сообщения. Можно было обойтись объектом, чтобы не бегать по массиву. Но на практике такие простые конфиги часто разрастаются.
То появится тултип с подробным описанием ошибки, то для каждой ошибки нужен будет свой цвет или еще какая-то фигня. Или вместо текстового кода мы решим использовать числовой.
В общем, чтобы не приходилось переделывать структуру конфига, сразу будем использовать массив объектов — в него добавлять новые поля безопасней.

Идем обратно в компонент BrandsNew. В теге script импортируем lodash и конфиг брендов

    import _ from 'lodash';
import config from '../configs/brands';

Создаем новое вычисляемое поле в computed

    errorMessage () {
return this.errorCode !== ''
? _.find(config.errors, { code: this.errorCode }).message
: '';
}

Это и есть сообщение об ошибке, которое берется из конфига по нужному коду. Осталось воткнуть его в разметку вместо errorCode. Вот так

    {{errorMessage}}

Вот теперь все. У нас есть рабочая схема, по которой мы обрабатываем и клиентские, и серверные ошибки, используя один формат данных.
Теперь чтобы добавить новую проверку, достаточно будет воткнуть ее в метод validate, если проверка клиентская, и в php-шный код, если серверная.
Главное, вернуть с бекенда json формата { code: ‘код ошибки’, … }. И добавить новый объект в конфиг, чтобы показывать адекватные тексты пользователям.

До встречи в следующих уроках.

Все уроки админки на vue.js

Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.

Истории из жизни айти и обсуждение кода.

Note: If you want to see how to handle these in React, take a look at my new post on that here — handling async errors with axios in react.

Whenever you’re making a backend API call with axios, you have to consider what to do with the .catch() block of your promise. Now, you might think that your API is highly available and it’ll be running 24/7. You might think that the user workflow is pretty clear, your javascript is sane, and you have unit tests. So when you stare at the catch block when making requests using axios you might think — «Well… I’ll just console.log it. That’ll be fine.»

axios.get('/my-highly-available-api').then(response => { // do stuff }) .catch(err => { // what now? console.log(err); })

But there are so many many many more things that are outside of your control that could throw errors when making API requests. And you probably don’t even know that they’re happening!

This post deals mainly with errors you see in the browser. Things can get pretty funny looking on the back end too — just take a look at 3 things you might see in your backend logs

Below are 3 types of errors that could appear, and how to handle it, when using axios.

Catching axios errors

Below is a snippet I’ve started including in a few JS projects.

axios.post(url, data).then(res => { // do good things }) .catch(err => { if (err.response) { // client received an error response (5xx, 4xx) } else if (err.request) { // client never received a response, or request never left } else { // anything else } })

Each condition is meant to capture a different type of error.

Checking error.response

If your error object contains a response field, that means your server responded with a 4xx/5xx error. Usually this is the error we’re most familiar with, and is most straightforward to handle.

Do things like a show a 404 Not Found page/error message if your API returns a 404. Show a different error message if your backend is returning a 5xx or not returning anything at all. You might think your well-architected backend won’t throw errors — but its a matter of when, not if.

Checking error.request

The second class of errors is where you don’t have a response but there’s a request field attached to the error. When does this happen? This happens when the browser was able to make a request, but for some reason, it didn’t see a response.

This can happen if:

  • you’re in a spotty network (think underground subway, or a building wireless network)
  • if your backend is hanging on each request and not returning a response on time
  • if you are making cross-domain requests and you’re not authorized to make the request
  • if you’re making cross-domain requests and you are authorized, but the backend API returns an error

One of the more common versions of this error had a message «Network Error» which is a useless error message. We have a front-end and a backend api hosted on different domains, so every backend API call is a cross-domain request.

Due to security constraints on JS in the browser, if you make an API request, and it fails due to crappy networks, the only error you’ll see is «Network Error» which is incredibly unhelpful. It can mean anything from «your device doesn’t have internet connectivity» to «your OPTIONS returned a 5xx» (if you make CORS requests). The reason for «Network Error» is described well here on this StackOverflow answer

All the other types of errors

If your error object doesn’t have the response or request field on it, that means its not an axios error and theres likely something else wrong in your app. The error message + stack trace should help you figure out where its coming from.

How do you fix it?

Degrading the user experience

This all depends on your app. For the projects I work on, for each of the features that use those endpoints, we degrade the user experience.

For example, if the request fails, and the page is useless without that data, then we have a bigger error page that will appear and offer users a way out — which sometimes is only a «Refresh the page» button.

Another example, if a request fails for a profile picture in a social media stream, we can show a placeholder image and disable profile picture changes, along with a toaster message explaining why the «update profile picture» button is disabled. However, showing an alert saying «422 Unprocessable Entity» is useless to see as a user.

Spotty networks

The web client I work on is used in school networks which can be absolutely terrible. The availability of your backend barely has anything to do with it, the request sometimes fail to leave the school network.

To solve these types of intermittent network problems, we added in axios-retry. This solved a good amount of the errors we were seeing in production. We added this to our axios setup:

const _axios = require('axios') const axiosRetry = require('axios-retry') const axios = _axios.create() // https://github.com/softonic/axios-retry/issues/87 const retryDelay = (retryNumber = 0) => { const seconds = Math.pow(2, retryNumber) * 1000; const randomMs = 1000 * Math.random(); return seconds + randomMs; }; axiosRetry(axios, { retries: 2, retryDelay, // retry on Network Error & 5xx responses retryCondition: axiosRetry.isRetryableError, }); module.exports = axios;

We were able to see that 10% of our users (which are in crappy school networks) were seeing sporadic «Network Errors» and that dropped down to <2% after adding in automatic retries on failure.

chart showing count of Network Errors

^ This is a screenshot of our errors is the count of «Network Errors» as they appear in NewRelic and are showing <1% of requests are erroring out.

Which leads to my last point:

Add error reporting to your front-end

Its helpful to have front-end error/event reporting so that you know whats happening in prod before your users tell you. At my day job, we use NewRelic Browser (paid service — no affiliation, just a fan) to send error events from the front-end. So whenever we catch an exception, we log the error message, along with the stack trace (although thats sometimes useless with minified bundles), and some metadata about the current session so we can try to recreate it.

Other tools that fill this space are Sentry + browser SDK, Rollbar, and a whole bunch of other useful ones listed here

Wrapping up

If you get nothing else out of this, do one thing.

Go to your code base now, and review how you’re handling errors with axios.

  • Check if you’re doing automatic retries, and consider adding axios-retry if you aren’t
  • Check that you’re catching errors, and letting the user know that something has happened. axios.get(...).catch(console.log) isn’t good enough.

So. How do you handle your errors? Let me know in the comments.

Логотип Vue
Vue.js

  • Обучение

    • Документация

      • Руководство
      • API
      • Рекомендации
      • Примеры
      • Книга рецептов
    • Видео курсы

      • Vue Mastery

      • Vue School

  • Экосистема

    • Помощь

      • Форум
      • Чат
      • Митапы
    • Инструментарий

      • Инструменты разработчика
      • Vue CLI
      • Vue Loader
    • Официальные плагины

      • Vue Router
      • Vuex
      • Vue Server Renderer
    • Новости

      • Еженедельные новости
      • Roadmap
      • События
      • Twitter
      • Блог
      • Вакансии
      • Сообщество разработчиков
  • Команда
  • Ресурсы

    • Партнёры
    • Темы
    • Awesome Vue
    • Найти пакеты для Vue
  • Поддержать Vue

    • Единоразовые пожертвования
    • Повторяющиеся взносы
    • Магазин футболок
  • Переводы

    • English
    • 中文
    • 日本語
    • 한국어
    • Português
    • Français
    • Tiếng Việt
    • Español
    • Bahasa Indonesia

Эта документация для версий v2.x и ранее.
Для v3.x, документация на русском здесь.

Используем axios для доступа к API

Простой пример

Неоднократно при создании веб-приложения вам может понадобиться получать и отображать данные из API. Существует несколько способов сделать это, но наиболее популярным решением является использование axios, основанного на Promise HTTP-клиента.

В этом упражнении мы будем использовать CoinDesk API для отображения цен на Биткойн, обновляемых каждую минуту. Прежде всего, подключим axios с помощью npm, yarn или ссылки на CDN.

Существует множество вариантов, как мы можем запрашивать информацию из API, но прежде необходимо узнать, в каком виде предоставляются данные, чтобы понимать, как их отображать. Для этого сделаем запрос к конечной точке (endpoint) API и выведем результат. Как можно убедиться из документации API CoinDesk, для получения данных мы будем делать запрос на https://api.coindesk.com/v1/bpi/currentprice.json. Изначально необходимо создать свойство в data для хранения нашей информации, далее извлечём и сохраним данные, используя хук жизненного цикла mounted.

new Vue({
el: '#app',
data() {
return {
info: null
};
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response));
}
});
<div id="app">
{{ info }}
</div>

И вот что мы получаем:

Посмотрите Pen First Step Axios and Vue by Vue (@Vue) на CodePen.

Отлично! Мы получили какие-то данные. Но сейчас они выглядят достаточно грязно, так что давайте отобразим их должным образом и добавим обработку ошибок на случай, если что-то пойдёт не так, либо когда для получения информации потребовалось больше времени.

Пример из жизни: работа с данными

Отображение данных из API

Типичная ситуация, когда необходимая информация находится внутри ответа сервера и нам необходимо знать структуру данных для получения доступа. В нашем случае, нужная информация о цене находится в response.data.bpi. Если мы используем это, то запрос будет выглядеть следующим образом.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi));

Посмотрите Pen Second Step Axios and Vue by Vue (@Vue) на CodePen.

Такие данные намного проще отображать, так что мы можем сейчас обновить HTML-разметку для отображения только нужной информации из полученных данных. Создадим фильтр, чтобы десятичные значения всегда отображались как нужно.

<div id="app">
<h1>Bitcoin Price Index</h1>
<div
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>
</div>
filters: {
currencydecimal (value) {
return value.toFixed(2)
}
}

Посмотрите Pen Third Step Axios and Vue by Vue (@Vue) на CodePen.

Обработка ошибок

Бывают моменты, когда мы не получили необходимые данные из API. Может быть множество причин, из-за которых вызов axios мог закончиться неудачей, например:

  • API не был доступен.
  • Запрос был сделан неправильно.
  • API не предоставил данные в ожидаемом нами формате.

При выполнении этого запроса мы должны проверять такие обстоятельства и предоставлять информацию в каждом случае, чтобы мы знали, как справиться с этой проблемой. В вызове axios мы сделаем это, используя catch.

axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => (this.info = response.data.bpi))
.catch(error => console.log(error));

Так мы узнаем, если что-то пойдёт не так во время запроса к API. Но что если данные повреждены или API не был доступен? Сейчас пользователь просто ничего не увидит. Мы могли бы использовать индикатор загрузки для этого случая и сообщать пользователю, что не можем получить данные.

new Vue({
el: '#app',
data() {
return {
info: null,
loading: true,
errored: false
};
},
filters: {
currencydecimal(value) {
return value.toFixed(2);
}
},
mounted() {
axios
.get('https://api.coindesk.com/v1/bpi/currentprice.json')
.then(response => {
this.info = response.data.bpi;
})
.catch(error => {
console.log(error);
this.errored = true;
})
.finally(() => (this.loading = false));
}
});
<div id="app">
<h1>Bitcoin Price Index</h1>

<section v-if="errored">
<p>We're sorry, we're not able to retrieve this information at the moment, please try back later</p>
</section>

<section v-else>
<div v-if="loading">Loading...</div>

<div
v-else
v-for="currency in info"
class="currency"
>
{{ currency.description }}:
<span class="lighten">
<span v-html="currency.symbol"></span>{{ currency.rate_float | currencydecimal }}
</span>
</div>

</section>
</div>

Вы можете нажимать на кнопку перезапуска (находится в правом нижнем углу во вкладке Result), чтобы увидеть индикатор загрузки во время получения данных из API.

Посмотрите Pen Fourth Step Axios and Vue by Vue (@Vue) на CodePen.

В дальнейшем код можно улучшить с помощью компонентов для различных секций и более детальными отчётами об ошибках. Это зависит от используемого API и сложности вашего приложения.

Альтернативы

Fetch API

Fetch API — мощный нативный API для создания запросов. Может вы слышали, что одно из преимуществ Fetch API в том, что не нужно загружать внешние зависимости для его использования, что является правдой! Однако… он ещё не полностью поддерживается браузерами, поэтому всё равно необходимо использовать полифил. Есть подводные камни при работе с его API, поэтому многие сейчас предпочитают axios. В будущем это может измениться.

Если вы заинтересовались Fetch API — существуют очень хорошие статьи, где объясняются тонкости его использования.

Итоги

Существует множество способов работы с Vue и axios, выходящие за рамки получения и отображения данных из API. Вы можете также взаимодействовать с бессерверными функциями (Serverless Functions), публикацией/редактированием/удалением через API, к которому вы имеете доступ, и т.д. Простая интеграция этих двух библиотек сделала axios очень распространённым выбором среди разработчиков, которым необходимо интегрировать HTTP-клиенты в их приложения.

Introduction

I really love the problem/solution. approach. We see some problem, and then, a really nice solution. But for this talking, i think we need some introduction as well.

When you develop an web application, you generally want’s to separate the frontend and backend. Fo that, you need something that makes the communication between these guys.

To illustrate, you can build a frontend (commonly named as GUI or user interface) using vanilla HTML, CSS and Javascript, or, frequently, using several frameworks like Vue, React and so many more avaliable online. I marked Vue because it’s my personal preference.

Why? I really don’t study the others so deeply that i can’t assure to you that Vue is the best, but i liked the way he works, the syntax, and so on. It’s like your crush, it’s a personal choice.

But, beside that, any framework you use, you will face the same problem:_ How to communicate with you backend_ (that can be written in so many languages, that i will not dare mention some. My current crush? Python an Flask).

One solution is to use AJAX (What is AJAX? Asynchronous JavaScript And XML). You can use XMLHttpRequest directly, to make requests to backend and get the data you need, but the downside is that the code is verbose. You can use Fetch API that will make an abstraction on top of XMLHttpRequest, with a powerfull set of tools. Other great change is that Fetch API will use Promises, avoiding the callbacks from XMLHttpRequest (preventing the callback hell).

Alternatively, we have a awesome library named Axios, that have a nice API (for curiosity purposes, under the hood, uses XMLHttpRequest, giving a very wide browser support). The Axios API wraps the XMLHttpRequest into Promises, different from Fetch API. Beside that, nowadays Fetch API is well supported by the browsers engines available, and have polyfills for older browsers. I will not discuss which one is better because i really think is personal preference, like any other library or framework around. If you dont’t have an opinion, i suggest that you seek some comparisons and dive deep articles. Has a nice article that i will mention to you written by Faraz Kelhini.

My personal choice is Axios because have a nice API, has Response timeout, automatic JSON transformation, and Interceptors (we will use them in the proposal solution), and so much more. Nothing that cannot be accomplished by Fetch API, but has another approach.

The Problem

Talking about Axios, a simple GET HTTP request can be made with these lines of code:

import axios from 'axios'

//here we have an generic interface with basic structure of a api response:
interface HttpResponse<T> {
  data: T[]
}

// the user interface, that represents a user in the system
interface User {
  id: number
  email: string
  name: string
}

//the http call to Axios
axios.get<HttpResponse<User>>('/users').then((response) => {
  const userList = response.data
  console.log(userList)
})

Enter fullscreen mode

Exit fullscreen mode

We’ve used Typescript (interfaces, and generics), ES6 Modules, Promises, Axios and Arrow Functions. We will not touch them deeply, and will presume that you already know about them.

So, in the above code, if everything goes well, aka: the server is online, the network is working perfectly, so on, when you run this code you will see the list of users on console. The real life isn’t always perfect.

We, developers, have a mission:

Make the life of users simple!

So, when something is go bad, we need to use all the efforts in ours hands to resolve the problem ourselves, without the user even notice, and, when nothing more can be done, we have the obligation to show them a really nice message explaining what goes wrong, to easy theirs souls.

Axios like Fetch API uses Promises to handle asynchronous calls and avoid the callbacks that we mention before. Promises are a really nice API and not to difficult to understand. We can chain actions (then) and error handlers (catch) one after another, and the API will call them in order. If an Error occurs in the Promise, the nearest catch is found and executed.

So, the code above with basic error handler will become:

import axios from 'axios'

//..here go the types, equal above sample.

//here we call axios and passes generic get with HttpResponse<User>.
axios
  .get<HttpResponse<User>>('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  .catch((error) => {
    //try to fix the error or
    //notify the users about somenthing went wrong
    console.log(error.message)
  })

Enter fullscreen mode

Exit fullscreen mode

Ok, and what is the problem then? Well, we have a hundred errors that, in every API call, the solution/message is the same. For curiosity, Axios show us a little list of them: ERR_FR_TOO_MANY_REDIRECTS, ERR_BAD_OPTION_VALUE, ERR_BAD_OPTION, ERR_NETWORK, ERR_DEPRECATED, ERR_BAD_RESPONSE, ERR_BAD_REQUEST, ERR_CANCELED, ECONNABORTED, ETIMEDOUT. We have the HTTP Status Codes, where we found so many errors, like 404 (Page Not Found), and so on. You get the picture. We have too much common errors to elegantly handle in every API request.

The very ugly solution

One very ugly solution that we can think of, is to write one big ass function that we increment every new error we found. Besides the ugliness of this approach, it will work, if you and your team remember to call the function in every API request.

function httpErrorHandler(error) {
  if (error === null) throw new Error('Unrecoverable error!! Error is null!')
  if (axios.isAxiosError(error)) {
    //here we have a type guard check, error inside this if will be treated as AxiosError
    const response = error?.response
    const request = error?.request
    const config = error?.config //here we have access the config used to make the api call (we can make a retry using this conf)

    if (error.code === 'ERR_NETWORK') {
      console.log('connection problems..')
    } else if (error.code === 'ERR_CANCELED') {
      console.log('connection canceled..')
    }
    if (response) {
      //The request was made and the server responded with a status code that falls out of the range of 2xx the http status code mentioned above
      const statusCode = response?.status
      if (statusCode === 404) {
        console.log('The requested resource does not exist or has been deleted')
      } else if (statusCode === 401) {
        console.log('Please login to access this resource')
        //redirect user to login
      }
    } else if (request) {
      //The request was made but no response was received, `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in Node.js
    }
  }
  //Something happened in setting up the request and triggered an Error
  console.log(error.message)
}

Enter fullscreen mode

Exit fullscreen mode

With our magical badass function in place, we can use it like that:

import axios from 'axios'

axios
  .get('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  .catch(httpErrorHandler)

Enter fullscreen mode

Exit fullscreen mode

We have to remember to add this catch in every API call, and, for every new error that we can graciously handle, we need to increase our nasty httpErrorHandler with some more code and ugly if's.

Other problem we have with this approach, besides ugliness and lack of mantenability, is that, if in one, only single one API call, i desire to handle different from global approach, i cannot do.

The function will grow exponentially as the problems that came together. This solution will not scale right!

The elegant and recommended solution

When we work as a team, to make them remember the slickness of every piece of software is hard, very hard. Team members, come and go, and i do not know any documentation good enough to surpass this issue.

In other hand, if the code itself can handle these problems on a generic way, do-it! The developers cannot make mistakes if they need do nothing!

Before we jump into code (that is what we expect from this article), i have the need to speak some stuff to you understand what the codes do.

Axios allow we to use something called Interceptors that will be executed in every request you make. It’s a awesome way of checking permission, add some header that need to be present, like a token, and preprocess responses, reducing the amount of boilerplate code.

We have two types of Interceptors. Before (request) and After (response) an AJAX Call.

It’s use is simple as that:

//Intercept before request is made, usually used to add some header, like an auth
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    const token = window.localStorage.getItem('token') //do not store token on localstorage!!!
    config.headers.Authorization = token
    return config
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error)
  }
)

Enter fullscreen mode

Exit fullscreen mode

But, in this article, we will use the response interceptor, because is where we want to deal with errors. Nothing stops you to extend the solution to handle request errors as well.

An simple use of response interceptor, is to call ours big ugly function to handle all sort of errors.

As every form of automatic handler, we need a way to bypass this (disable), when we want. We are gonna extend the AxiosRequestConfig interface and add two optional options raw and silent. If raw is set to true, we are gonna do nothing. silent is there to mute notifications that we show when dealing with global errors.

declare module 'axios' {
  export interface AxiosRequestConfig {
    raw?: boolean
    silent?: boolean
  }
}

Enter fullscreen mode

Exit fullscreen mode

Next step is to create a Error class that we will throw every time we want to inform the error handler to assume the problem.

export class HttpError extends Error {
  constructor(message?: string) {
    super(message) // 'Error' breaks prototype chain here
    this.name = 'HttpError'
    Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
  }
}

Enter fullscreen mode

Exit fullscreen mode

Now, let’s write the interceptors:

// this interceptor is used to handle all success ajax request
// we use this to check if status code is 200 (success), if not, we throw an HttpError
// to our error handler take place.
function responseHandler(response: AxiosResponse<any>) {
  const config = response?.config
  if (config.raw) {
    return response
  }
  if (response.status == 200) {
    const data = response?.data
    if (!data) {
      throw new HttpError('API Error. No data!')
    }
    return data
  }
  throw new HttpError('API Error! Invalid status code!')
}

function responseErrorHandler(response) {
  const config = response?.config
  if (config.raw) {
    return response
  }
  // the code of this function was written in above section.
  return httpErrorHandler(response)
}

//Intercept after response, usually to deal with result data or handle ajax call errors
const axiosDefaults = {}
const http = axios.create(axiosDefaults)
//register interceptor like this
http.interceptors.response.use(responseHandler, responseErrorHandler)

Enter fullscreen mode

Exit fullscreen mode

Well, we do not need to remember our magical badass function in every ajax call we made. And, we can disable when we want, just passing raw to request config.

import axios from 'axios'

// automagically handle error
axios
  .get('/users')
  .then((response) => {
    const userList = response.data
    console.log(userList)
  })
  //.catch(httpErrorHandler) this is not needed anymore

// to disable this automatic error handler, pass raw
axios
  .get('/users', {raw: true})
  .then((response) => {
    const userList = response.data
    console.log(userList)
  }).catch(() {
    console.log("Manually handle error")
  })

Enter fullscreen mode

Exit fullscreen mode

Ok, this is a nice solution, but, this bad-ass ugly function will grow so much, that we cannot see the end. The function will become so big, that anyone will want to maintain.

Can we improve more? Oh yeahhh.

The IMPROVED and elegant solution

We are gonna develop an Registry class, using Registry Design Pattern. The class will allow you to register error handling by an key (we will deep dive in this in a moment) and a action, that can be an string (message), an object (that can do some nasty things) or an function, that will be executed when the error matches the key. The registry will have parent that can be placed to allow you override keys to custom handle scenarios.

Here are some types that we will use througth the code:

// this interface is the default response data from ours api
interface HttpData {
  code: string
  description?: string
  status: number
}

// this is all errrors allowed to receive
type THttpError = Error | AxiosError | null

// object that can be passed to our registy
interface ErrorHandlerObject {
  after?(error?: THttpError, options?: ErrorHandlerObject): void
  before?(error?: THttpError, options?: ErrorHandlerObject): void
  message?: string
  notify?: QNotifyOptions
}

//signature of error function that can be passed to ours registry
type ErrorHandlerFunction = (error?: THttpError) => ErrorHandlerObject | boolean | undefined

//type that our registry accepts
type ErrorHandler = ErrorHandlerFunction | ErrorHandlerObject | string

//interface for register many handlers once (object where key will be presented as search key for error handling
interface ErrorHandlerMany {
  [key: string]: ErrorHandler
}

// type guard to identify that is an ErrorHandlerObject
function isErrorHandlerObject(value: any): value is ErrorHandlerObject {
  if (typeof value === 'object') {
    return ['message', 'after', 'before', 'notify'].some((k) => k in value)
  }
  return false
}

Enter fullscreen mode

Exit fullscreen mode

So, with types done, let’s see the class implementation. We are gonna use an Map to store object/keys and a parent, that we will seek if the key is not found in the current class. If parent is null, the search will end. On construction, we can pass an parent,and optionally, an instance of ErrorHandlerMany, to register some handlers.

class ErrorHandlerRegistry {
  private handlers = new Map<string, ErrorHandler>()

  private parent: ErrorHandlerRegistry | null = null

  constructor(parent: ErrorHandlerRegistry = undefined, input?: ErrorHandlerMany) {
    if (typeof parent !== 'undefined') this.parent = parent
    if (typeof input !== 'undefined') this.registerMany(input)
  }

  // allow to register an handler
  register(key: string, handler: ErrorHandler) {
    this.handlers.set(key, handler)
    return this
  }

  // unregister a handler
  unregister(key: string) {
    this.handlers.delete(key)
    return this
  }

  // search a valid handler by key
  find(seek: string): ErrorHandler | undefined {
    const handler = this.handlers.get(seek)
    if (handler) return handler
    return this.parent?.find(seek)
  }

  // pass an object and register all keys/value pairs as handler.
  registerMany(input: ErrorHandlerMany) {
    for (const [key, value] of Object.entries(input)) {
      this.register(key, value)
    }
    return this
  }

  // handle error seeking for key
  handleError(
    this: ErrorHandlerRegistry,
    seek: (string | undefined)[] | string,
    error: THttpError
  ): boolean {
    if (Array.isArray(seek)) {
      return seek.some((key) => {
        if (key !== undefined) return this.handleError(String(key), error)
      })
    }
    const handler = this.find(String(seek))
    if (!handler) {
      return false
    } else if (typeof handler === 'string') {
      return this.handleErrorObject(error, { message: handler })
    } else if (typeof handler === 'function') {
      const result = handler(error)
      if (isErrorHandlerObject(result)) return this.handleErrorObject(error, result)
      return !!result
    } else if (isErrorHandlerObject(handler)) {
      return this.handleErrorObject(error, handler)
    }
    return false
  }

  // if the error is an ErrorHandlerObject, handle here
  handleErrorObject(error: THttpError, options: ErrorHandlerObject = {}) {
    options?.before?.(error, options)
    showToastError(options.message ?? 'Unknown Error!!', options, 'error')
    return true
  }

  // this is the function that will be registered in interceptor.
  resposeErrorHandler(this: ErrorHandlerRegistry, error: THttpError, direct?: boolean) {
    if (error === null) throw new Error('Unrecoverrable error!! Error is null!')
    if (axios.isAxiosError(error)) {
      const response = error?.response
      const config = error?.config
      const data = response?.data as HttpData
      if (!direct && config?.raw) throw error
      const seekers = [
        data?.code,
        error.code,
        error?.name,
        String(data?.status),
        String(response?.status),
      ]
      const result = this.handleError(seekers, error)
      if (!result) {
        if (data?.code && data?.description) {
          return this.handleErrorObject(error, {
            message: data?.description,
          })
        }
      }
    } else if (error instanceof Error) {
      return this.handleError(error.name, error)
    }
    //if nothings works, throw away
    throw error
  }
}
// create ours globalHandlers object
const globalHandlers = new ErrorHandlerRegistry()

Enter fullscreen mode

Exit fullscreen mode

Let’s deep dive the resposeErrorHandler code. We choose to use key as an identifier to select the best handler for error. When you look at the code, you see that has an order that key will be searched in the registry. The rule is, search for the most specific to the most generic.

const seekers = [
  data?.code, //Our api can send an error code to you personalize the error messsage.
  error.code, //The AxiosError has an error code too (ERR_BAD_REQUEST is one).
  error?.name, //Error has a name (class name). Example: HttpError, etc..
  String(data?.status), //Our api can send an status code as well.
  String(response?.status), //respose status code. Both based on Http Status codes.
]

Enter fullscreen mode

Exit fullscreen mode

This is an example of an error sent by API:

{
  "code": "email_required",
  "description": "An e-mail is required",
  "error": true,
  "errors": [],
  "status": 400
}

Enter fullscreen mode

Exit fullscreen mode

Other example, as well:

{
  "code": "no_input_data",
  "description": "You doesnt fill input fields!",
  "error": true,
  "errors": [],
  "status": 400
}

Enter fullscreen mode

Exit fullscreen mode

So, as an example, we can now register ours generic error handling:

globalHandlers.registerMany({
  //this key is sent by api when login is required
  login_required: {
    message: 'Login required!',
    //the after function will be called when the message hides.
    after: () => console.log('redirect user to /login'),
  },
  no_input_data: 'You must fill form values here!',
  //this key is sent by api on login error.
  invalid_login: {
    message: 'Invalid credentials!',
  },
  '404': { message: 'API Page Not Found!' },
  ERR_FR_TOO_MANY_REDIRECTS: 'Too many redirects.',
})

// you can registre only one:
globalHandlers.register('HttpError', (error) => {
  //send email to developer that api return an 500 server internal console.error
  return { message: 'Internal server errror! We already notify developers!' }
  //when we return an valid ErrorHandlerObject, will be processed as whell.
  //this allow we to perform custom behavior like sending email and default one,
  //like showing an message to user.
})

Enter fullscreen mode

Exit fullscreen mode

We can register error handler in any place we like, group the most generic in one typescript file, and specific ones inline. You choose. But, to this work, we need to attach to ours http axios instance. This is done like this:

function createHttpInstance() {
  const instance = axios.create({})
  const responseError = (error: any) => globalHandlers.resposeErrorHandler(error)
  instance.interceptors.response.use(responseHandler, responseError)
  return instance
}

export const http: AxiosInstance = createHttpInstance()

Enter fullscreen mode

Exit fullscreen mode

Now, we can make ajax requests, and the error handler will work as expected:

import http from '/src/modules/http'

// automagically handle error
http.get('/path/that/dont/exist').then((response) => {
  const userList = response.data
  console.log(userList)
})

Enter fullscreen mode

Exit fullscreen mode

The code above will show a Notify ballon on the user screen, because will fire the 404 error status code, that we registered before.

Customize for one http call

The solution doesn’t end here. Let’s assume that, in one, only one http request, you want to handle 404 differently, but just 404. For that, we create the dealsWith function below:

export function dealWith(solutions: ErrorHandlerMany, ignoreGlobal?: boolean) {
  let global
  if (ignoreGlobal === false) global = globalHandlers
  const localHandlers = new ErrorHandlerRegistry(global, solutions)
  return (error: any) => localHandlers.resposeErrorHandler(error, true)
}

Enter fullscreen mode

Exit fullscreen mode

This function uses the ErrorHandlerRegistry parent to personalize one key, but for all others, use the global handlers (if you wanted that, ignoreGlobal is there to force not).

So, we can write code like this:

import http from '/src/modules/http'

// this call will show the message 'API Page Not Found!'
http.get('/path/that/dont/exist')

// this will show custom message: 'Custom 404 handler for this call only'
// the raw is necessary because we need to turn off the global handler.
http.get('/path/that/dont/exist', { raw: true }).catch(
  dealsWith({
    404: { message: 'Custom 404 handler for this call only' },
  })
)

// we can turn off global, and handle ourselves
// if is not the error we want, let the global error take place.
http
  .get('/path/that/dont/exist', { raw: true })
  .catch((e) => {
    //custom code handling
    if (e.name == 'CustomErrorClass') {
      console.log('go to somewhere')
    } else {
      throw e
    }
  })
  .catch(
    dealsWith({
      404: { message: 'Custom 404 handler for this call only' },
    })
  )

Enter fullscreen mode

Exit fullscreen mode

The Final Thoughts

All this explanation is nice, but code, ah, the code, is so much better. So, i’ve created an github repository with all code from this article organized to you try out, improve and customize.

  • Click here to access the repo in github.

FOOTNOTES:

  • This post became so much bigger than a first realize, but i love to share my thoughts.
  • If you have some improvement to the code, please let me know in the comments.
  • If you see something wrong, please, fix-me!

Error handling within an SPA can be challenging, most of the app uses some sort of backend service to store the app data, and things can go wrong due to network connection or a bad user Input. Working with Vue.js app I have figured that to handle all the API related errors best bet is to use axios inteceptor.

Typical error handling

Since axios is promise based HTTP library we can handle any error using then(response) and catch(error), but if you are not careful you will end up scattering catch() block throughout your application. In most cases, you will be showing some type of alert message to the screen.

axios.get('/user/1').then((response) => {
    console.log('Everything is awesome.');
}).catch((error) => {
    console.warn('Not good man :(');
})

It’s good for a small app but as your apps start growing it will be messy to maintain it, and if you decided to change the way you show errors to a user you will be doing a lot of work to update in all places.

Interceptor Error handling

Here comes the clean way of handling errors with axios interceptors. Idea is to check the error response and define some set of rules to show the errors on the global level. With this you don’t need the catch(error) block where you make axios call to handle request errors, all the error will be handled by interceptor.

Add following in resources/assets/js/bootstrap.js after axios import.

axios.interceptors.response.use(
function(response) { return response;}, 
function(error) {
    // handle error
    if (error.response) {
        alert(error.response.data.message);
    }
});

Error Notification

Just plain alert() box is not a great way to show errors, you cannot customize them and they look very ugly. I got this iziToast library which is pretty neat with lots of configuration and sweat animation backed in.

We can use it directly like this in our app but I prefer to create a wrapper around it so If some reason I need to change the notification system later I can just update the wrapper and everything will work with new hotter toast 2.0 notification in future 😎

Now pull the iziToast using npm and create a resources/assets/js/services/toast.js and add following code.

import 'izitoast/dist/css/iziToast.min.css'
import iZtoast from 'izitoast'

const toast = {
    error: (message, title = 'Error') => {
        return iZtoast.error({
            title: title,
            message: message,
            position: 'bottomCenter'
        });
    },
    success: (message, title = 'Success') => {
        return iZtoast.success({
            title: title,
            message: message,
            position: 'bottomCenter'
        });
    }
};

export default toast;

For simplicity, I am keeping only error and success type notification, but you can add all the features your app needed for notification.

Now let’s update the interceptor to use our toast notification instead boring alert box.

import toast from './toast'

axios.interceptors.response.use(
...
function(error) {
    // handle error
    if (error.response) {
        toast.error(error.response.data.message);
    }
});

Turn off error handling

I came across many scenarios where a global error handler was not a good choice. For example, if there is a request which error format is different or unique like currently, we are expecting message key in error response which is default in Laravel for any error. But if you are making calls to different API provider or you want to customize the notification, what to do then? Don’t worry axios configuration can help with this.

axios.get('/user/1', {errorHandle: false}).then((response) => {
    console.log('Everything is awesome.');
}).catch((error) => {
    // handle this error here
    alert('Not good man :(');
})

By passing a config property {errorHandle: false} in axios request we can turn off the error handling for this call. Now let’s modify the interceptor to make it happen.

axios.interceptors.response.use(
function (response) {
    return response;
}, 
function(error) {
    // check for errorHandle config
    if( error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false ) {
        return Promise.reject(error);
    }

    if (error.response) {
        toast.error(error.response.data.message);
    }
});

This makes it very flexible, now you can turn off global error handling for a specific endpoint call and you can use catch(error) to handle the error.

Error handler module

Since all the app needs this error handling let’s put it in a module so you can reuse it in any vuejs app or any app in general which uses axios to make HTTP calls.

Create resources/assets/js/services/errorHandler.js and add following code

import axios from 'axios'
import toast from './toast'

function errorResponseHandler(error) {
    // check for errorHandle config
    if( error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false ) {
        return Promise.reject(error);
    }

    // if has response show the error 
    if (error.response) {
        toast.error(error.response.data.message);
    }
}

// apply interceptor on response
axios.interceptors.response.use(
   response => response,
   errorResponseHandler
);

export default errorResponseHandler;

This module is applying the response interceptor to handle the errors during a request. To use it just import it in your resources/assets/js/bootstrap.js like this.

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

require('./services/errorHandler');

If you want to Display Form input error, do check out the vue directive which is based on a similar implementation.

I hope it gives you a clean way to handle the error in your API, let me know in the comments, see you next time.

Source Code

In the course of fetching APIs, if the API request doesn’t go as planned, we may encounter errors. Let’s see how to manage these errors using Axios.

If you don’t want to use the built-in Fetch API, you can opt for the many 3rd party libraries available on npm, and Axios is the most popular among them. It is essentially a wrapper around the Fetch API and allows you to make HTTP requests using a promise-based HTTP client.

API Errors

When a request to an API doesn’t go as planned, an API error occurs. So, the API must respond to the client specifying whether the request was successful or not. In this case, we should send an error response, and it is the only way for the developers to diagnose what went wrong. HTTP Status Codes are used for this purpose. The following status codes notify about the errors.

  • 4xx — Client error. Such as 404: Requested URL not found.
  • 5xx — Server error.

Informing the client about the error helps them understand the error and its cause.

Using Axios

To handle errors in a standard API call using Axios, we use a try...catch block. For example, take a look at the following code, which fetches random quotes from the Famous Quotes API from RapidAPI Hub.

js

import axios from 'axios';

const fetchQuotes = async () => {

try {

const res = await axios.get(

`https://famous-quotes4.p.rapidapi.com/random`

);

} catch (error) {

// Do something with the error here

}

};

If an error occurs, the catch block captures it. We need to add some logic in this block to handle the errors. We have to take care of three scenarios of errors:

  1. Request is made, but the server responds with an error.

  2. Request is made, but no response is received from the server.

  3. When an error occurs while setting up the request.

To handle these scenarios, we can use an if-else block like this:

js

try {

const res = await axios.get(`https://famous-quotes4.p.rapidapi.com/random`);

} catch (error) {

if (error.response) {

// Request made but the server responded with an error

} else if (error.request) {

// Request made but no response is received from the server.

} else {

// Error occured while setting up the request

}

}

It is critical to check for the request and response properties because there will be no response property if we do not receive a response. Similarly, there will be no request property if the request is not set up. Let’s take a look at these properties.

error.response

If the request is made and the server gives an error response, the error object will have a response property. It means that a 4XX or 5XX error has occurred. The response object has many properties which we can log, like the status property, which has the status code of the error.

error.request

error.request is the request object of the HTTP request that the client made. It contains information such as the HTTP method, URL, and the headers sent with the request. For Axios, it is an instance of XMLHttpRequest when running in the browser and an instance of http.ClientRequest when executed in Node.js. We use it when we do not receive a valid response from the API due to a poor network or unauthorized access.

Logging Errors

Finally, we can use these properties to log errors properly. It will look like this in code:

js

try {

const res = await axios.get(`https://famous-quotes4.p.rapidapi.com/random`);

} catch (error) {

if (error.response) {

// Request made but the server responded with an error

console.log(error.response.data);

console.log(error.response.status);

console.log(error.response.headers);

} else if (error.request) {

// Request made but no response is received from the server.

console.log(error.request);

} else {

// Error occured while setting up the request

console.log('Error', error.message);

}

}

The same goes for Axios POST, PUT, PATCH, and all other types of requests. Now you know how to manage API errors using Axios. Find a suitable API from RapidAPI Hub and integrate it into your projects using Axios.

Introduction

Axios is a JavaScript library that uses the Promise API to create HTTP requests with http in Node.js runtime or XMLHttpRequests in the browser. Because these requests are promises, they work with the newer async/await syntax, as well as .then() functions for promise chaining and the .catch() mechanism for error handling.

try {
    let res = await axios.get('/my-api-route');

    // Work with the response...
} catch (err) {
    // Handle error
    console.log(err);
}

In this article, we will see how to handle errors with Axios, as this is very important when making any HTTP calls knowing fully well that there are times when the service you’re calling might not be available or return other unexpected errors. We’ll show the .then()/.catch() method, but primarily use the async/await syntax.

Then and Catch

Promises can be handled in two ways using modern JS — the async/await syntax, which was shown above, as well as .then() and .catch() methods. Note that both of these methods can produce the same functionality, but async/await is typically regarded as being easier to work with and requires less boilerplate code in longer promise chains.

Here is how you’d achieve the same thing, but using the then/catch method:

axios.get('/my-api-route')
    .then(res => {
        // Work with the response...
    }).catch(err => {
        // Handle error
        console.log(err);
    });

Both the res and err objects are the same as with the async/await syntax.

Handling Errors

In this section, we will look at two primary categories of problems, as well as other issues that we may encounter and how to manage them using Axios. It is critical that you understand that this applies to all types of HTTP queries handled by Axios, including GET, POST, PATCH, and so on.

Here you can see the syntax for the three aspects — this will capture the error; it is crucial to note that this error carries a large error object with a lot of information:

try {
    let res = await axios.get('/my-api-route');

    // Work with the response...
} catch (err) {
    if (err.response) {
        // The client was given an error response (5xx, 4xx)
    } else if (err.request) {
        // The client never received a response, and the request was never left
    } else {
        // Anything else
    }
}

The differences in the error object, highlighted above in the catch code, indicate where the request encountered the issue. We’ll look deeper into this in the following sections.

error.response

This is the type of mistake we are most familiar with, and it is much easier to deal with. Many sites display a 404 Not Found page/error message or various response codes based on what the API provides; this is often handled via the response.

If your error object has a response property, it signifies your server returned a 4xx/5xx error. This will assist you choose what sort of message to return to users; the message you’ll want to provide for 4xx may differ from that for 5xx, and if your backend isn’t returning anything at all.

try {
    let res = await axios.get('/my-api-route');

    // Work with the response...
} catch (err) {
    if (err.response) {
        // The client was given an error response (5xx, 4xx)
        console.log(err.response.data);
        console.log(err.response.status);
        console.log(err.response.headers);
    } else if (err.request) {
        // The client never received a response, and the request was never left
    } else {
        // Anything else
    }
}

error.request

This error is most commonly caused by a bad/spotty network, a hanging backend that does not respond instantly to each request, unauthorized or cross-domain requests, and lastly if the backend API returns an error.

Note: This occurs when the browser was able to initiate a request but did not receive a valid answer for any reason.

try {
    let res = await axios.get('/my-api-route');

    // Work with the response...
} catch (err) {
    if (err.response) {
        // The client was given an error response (5xx, 4xx)
    } else if (err.request) {
        // The client never received a response, and the request was never left
        console.log(err.request);
    } else {
        // Anything else
    }
}

Earlier we mentioned that the underlying request Axios uses depends on the environment in which it’s being run. This is also the case for the err.request object. Here the err.request object is an instance of XMLHttpRequest when being executed in the browser, whereas it’s an instance of http.ClientRequest when being used in Node.js.

Other Errors

It’s possible that the error object does not have either a response or request object attached to it. In this case it is implied that there was an issue in setting up the request, which eventually triggered an error.

try {
    let res = await axios.get('/my-api-route');

    // Work with the response...
} catch (err) {
    if (err.response) {
        // The client was given an error response (5xx, 4xx)
    } else if (err.request) {
        // The client never received a response, and the request was never left
    } else {
        // Anything else
        console.log('Error', err.message);
    }
}

For example, this could be the case if you omit the URL parameter from the .get() call, and thus no request was ever made.

Conclusion

In this short article, we looked at how we may handle various sorts of failures and errors in Axios. This is also important for giving the correct message to your application/website visitors, rather than always returning a generic error message, sending a 404, or indicating network problems.

Понравилась статья? Поделить с друзьями:
  • Vts ошибка а57
  • Vts ошибка al06
  • Vts ошибка a748
  • Vts ошибка a249
  • Vts ошибка a225