Обработка ошибок Axios
Привет, Хабр! Меня зовут Алёна, я senior фронтенд-разработчик отдела разработки ПО для розничного бизнеса в Райффайзенбанке. Недавно передо мной встала задача улучшения пользовательского опыта обработки ошибок запросов к бэкенду. Я решила комплексно исследовать эту тему на примере HTTP-клиента Axios.
Если при отправке запросов с помощью Axios возникает ошибка — клиентское приложение получает аргумент, который может быть экземпляром объекта, производного от системного класса Error, или любым типом. Он может содержать много информации, и не всегда понятно, что самое важное для определения типа исключения и способа обработки. Поэтому я выделала 4 категории ошибок запросов, сделанных при помощи Axios, которые нужно по-разному интерпретировать.
Фронтовая часть нашего приложения — это React-клиент, написанный на TypeScript. В качестве HTTP-клиента используется Axios. Это довольно популярная библиотека с удобным API. Для управления сетевыми запросами мы выбрали React Query. Эта библиотека увеличивает производительность обмена данных с сервером за счет кеширования, повторных попыток и других способов оптимизации. Однако данный подход может быть применен для стандартного fetch-запроса или других библиотек обмена данными.

4 категории ошибок запросов, сделанных при помощи Axios:
Запрос был обработан сервером, и статус ответа сервера вне диапазона 2xx.
Запрос был сделан, но ответ не был получен.
Запрос не был отправлен, но объект ошибки является инстансом AxiosError.
Ошибка не является инстансом AxiosError.
Документация Axios акцентирует внимание только на первых 3-х случаях. Но существует и 4-ый. Он может возникнуть, к примеру, если вы пытаетесь отправить объект с циклической ссылкой, который невозможно сериализовать:
import axios from 'axios';
const invalidData = { circularLink: {} };
// Создаем циклическую ссылку
invalidData.circularLink = invalidData;
axios
.post('http://somewebsite.com', invalidData)
.then(/*...*/)=>{
/*...*/
})
.catch((error) => {
console.error(error instanceof Error); // true
console.error(error.message); // Maximum call stack size exceeded
console.error(error.response); // undefined
console.error(error.request); // undefined
})
;
Перейдем к описанию алгоритма обработки ошибок. Для удобства реализуем его в виде статического класса. Но алгоритм может быть написан и в функциональном стиле, либо непосредственно в блоке catch на TypeScript или JavaScript. Так как в ответе сервер может прислать несколько ошибок, то возвращаем promise c массивом строк. Вот фрагмент кода на языке TypeScript:
import axios from 'axios';
export class ErrorUtils {
static async getErrors(error: any): Promise {
let errorMessages: string[];
// Проверим, что ошибка является инстансом AxiosError
if (axios.isAxiosError(error)) {
if (error.response){
/* Обрабатываем первый случай, когда был получен ответ от сервера и
и его статус вне диапазона 2хх. Метод _getResponseErrors описан ниже */
errorMessages = await this._getResponseError(/*...*/);
} else if (error.request) {
/* Обработаем второй случай, когда запрос сделан,
но ответ не был получен. Реализация метода _getRequertErrors
приведена ниже */
errorMessages = this._getRequestErrors(/*...*/);
} else {
/* 3ий случай, когда объект ошибки не содержит response и request,
но является инстансом AxiosError*/
errorMessages = [/*...*/];
}
} else {
/* 4ый случай. Ошибка не является инстансом AxiosError. Например,
при отправке объекта с циклической ссылкой */
errorMessages = this._handleNotAxiosError(/*...*/);
}
return errorMessages;
}
/*...*/
}
Теперь на примере этого метода разберём обработку всех 4-х категорий.
Категория 1. Запрос был обработан сервером и статус ответа сервера вне диапазона 2xx
При возникновении исключения обработки запроса наиболее информативным будет случай, когда сервер прислал ошибку с развернутым ответом. Например, при валидации данных:
import { AxiosError } from 'axios';
const axiosError1: AxiosError = {
response: {
status: 400,
// Пример модели ошибки
data: {
R34: ['Дата "до" должна быть не меньше сегодняшнего для']б
H85: ['Не указано имя', 'Не указан год рождения'],
},
/*...*/
}
/*...*/
};
const axiosError2: AxiosError = {
response: {
status: 422,
// Другой пример ошибки, которая может пройти с бекенда
data: {
message: 'Файл должен быть в формате XLSX',
}
/*...*/
},
/*...*/
};
Независимо от статуса ответа, первым этапом необходимо проверить, что поле response.data не пустое, и попробовать распознать полученную от сервера модель ошибки. В нашем случае мы используем общий обработчик ошибок, который прописали в конфигурации QueryClient, и при необходимости обрабатываем ошибки на уровне самого запроса. Например, если требуется показать ошибки определенным способом. Наш бэкенд использует микросервисную архитектуру, а также взаимодействует с другими сервисами банка, поэтому модели ошибок могут быть разными, и есть шанс, что придет еще незнакомая фронту структура данных.
Если у фронта не получилось распознать модель ошибки, которую бэкенд прислал в response.data, то нужно поискать подходящее сообщение в списке кодов HTTP-статусов.
С полным списком кодов состояния HTTP можно ознакомиться на Википедии.
В данном случае нужны статусы с кодом выше, чем 2хх.
Так как у нас описаны только самые часто встречающие статусы, то может возникнуть ситуация, когда приложение покажет общее сообщение для данного типа ошибок. Если описать все статусы, то этот пункт можно пропустить.
Для удобства показа ошибок с учетом микросервисной архитектуры мы определим имя сервиса на основе url и имя метода, которые прописываем в каждом обработчике.
private static async _getResponseErrors(
response: AxiosResponse: AxiosResponse, // error.response
message: string, // error.message
serviceName: string, // Получение имени сервиса будет описано ниже
): string[] {
// Пытаемся получить имя метода. Детали приведены ниже
const methodName = response.config?.description || '';
// Пытаемся распознать модель ошибки, которую прислал сервер
const errorMessages = await this._tryGetErrorFromData(response);
if (errorMessages) {
return errorMessages;
}
/* Ищем сообщение в списке статусов. Константа HTTP_ERROR_DESCRIPTIONS указана
в следующем примере кода */
const getErrorMessageFn = HTTP_ERROR_DESCRIPTIONS[response.status as HttpStatusCode];
if (getErrorMessageFn) {
return [getErrorMessageFn(serviceName, methodName)];
}
/* Оказываем общее сообщение, если не нашли расшифровку ошибки по статусу.
Если в списке HTTP_ERROR_DESCRIPTIONS описать все статусы, то этот шаг можно
пропустить */
return [
`При вызове метода ${methodName} произошла ошибка при получении ответа от сервиса ${serviceName}: ${message}`,
];
}
Для того чтобы определить имя сервиса, воспользуемся следующим методом:
private static _getServiceName(
baseURL?: string, // error.config?.baseURL
url?: string // error.config?.url
): string {
const serviceUrl = (baseURL || '') + (url || '');
// Константа SERVICE_NAMES приведена ниже
for (const key in SERVICE_NAMES) {
if (serviceUrl.indexOf(key) !== -1) {
return SERVICE_NAMES[key];
}
}
return '';
}
Далее необходимо реализовать метод, который пытается распознать модель ошибки и в случае успеха вернет список ошибок. Допустим, что мы ожидаем следующие 2 модели ошибок:
// 1ый пример модели ошибки
export interface ErrorsModel {
errors: Record;
}
// 2ой пример модели ошибки
export interface MessageErrorModel {
message: string;
}
Тогда метод обработки ошибок запроса будет выглядеть так:
private static async _tryGetErrorFromData(response: AxiosResponse) {
if (!response.data) {
return undefined;
}
const errorMessages = new Array();
/* Если бек прислал модель ошибки, как в 1ом примере.
В вашем приложении, скорее всего, будут другие модели ошибок. */
const errors = (response.data as ErrorsModel).errors;
if (errors) {
for (const errorKey in errors) {
const errorValue = errors[errorKey];
errorMessages = errorMessages.concat(errorValue);
}
return errorMessages;
}
/* Если бек прислал модель ошибки, как во 2м примере.
В вашем приложении, скорее всего, будут другие модели ошибок. */
const errorMessage = (response.data as MessageErrorModel).message;
if (errorMessage) {
return errorMessages.push(errorMessage);
}
// Здесь может быть обработка других моделей ошибок
/*...*/
return undefined;
}
Приведем пример константы SERVICE_NAMES, используемой для получения имен сервисов:
const SERVICE_NAMES: Record = {
'/customer-accounts/': 'получения данных по счетам',
/*...*/
}
В описании названий сервисов удобно пропустить слово «сервис», чтобы в описании ошибки использовать его в разных падежах.
Константу HTTP_ERROR_DESCRIPTIONS, содержащую описание ошибок, также можно сделать типа Record:
export const HTTP_ERROR_DESCRIPTIONS: Partial<
Record string>
> = {
[HttpStatusCode.BadRequest]: (serviceName: string, methodName: string) =>
`Сервис ${serviceName} не смог понять запрос метода ${methodName} из-за некорректного синтаксиса`,
/*...*/
[HttpStatusCode.ServiceUnavailable]: (serviceName: string) =>
`Сервис ${serviceName} не доступен`,
/*...*/
};
Слово «метод» тоже можно пропустить, чтобы склонять его по падежам в сообщениях. А название метода в AxiosRequestConfig лучше указать в самом запросе.
export function getStatusHistory(id: number) {
return someAxiosServiceApi
.get(
`/some-url/${id}/status-history`,
/* Указываем описание метода в конфиге запроса. Данное свойство
не входит в интерфейс AxiosRequestConfig, поэтому ниже будет показано
как его перезаписать с помощью файла декларации типов */
{ description: 'получения истории статусов' }
)
.then(({ data }) => data);
}
Чтобы в TypeScript использовать дополнительное свойство в AxiosRequestConfig, добавим в проект файл декларации типов *.d.ts.
Обратите внимание! Для JavaScript этот и следующий шаг не требуется.
// код файла axios.d.ts
import 'axios';
declare module 'axios' {
export interface AxiosRequestConfig {
/* Описание должно быть без слова "метод". Например, 'получения списка счетов' */
description?: string;
}
}
В файле конфигурации TypeScript проектов tsconfig.json нужно прописать путь к файлу декларации типов *.d.ts. Это необходимо для корректной работы компилятора TypeScript кода:
{
"compilerOptions": {
/*...*/
/* Может протребоваться добавить jest и node для корректной работы
сборшика модулей*/
"types": ["./src/shared/types", "jest", "node"],
},
/*...*/
}
Категория 2. Запрос был сделан, но ответ не был получен
Если в объекте ошибки отсутствует свойство response, но присутствует свойство request, значит, запрос отправлен, но не получил ответ от сервера. Например, если вы пытаетесь выполнить запрос к серверу, который недоступен, или запрос превышает установленный таймаут и не получает ответа. В данном случае получить информацию о причине падения можно по полю code в инстансе ошибки:
private static _getRequestErrors(
serviceName: string,
code?: string // error.code
) {
if (code) {
/* Получаем сообщение об ошибке на основе кода ошибки */
const errorMessage = ERROR_DESCRIPTIONS[code](serviceName);
if (errorMessage) {
return [errorMessage];
}
}
/* Если описать полный перечень кодов в ERROR_DESCRIPTIONS, то
этот шаг можно пропустить */
return [`Не удалось получить ответ от сервиса ${serviceName}`];
}
Константа ERROR_DESCRIPTIONS, содержащая описание ошибок, может быть реализована следующим образом:
export const ERROR_DESCRIPTIONS: Record<
string,
(serviceName: string) => string
> = {
/*...*/
[AxiosError.ERR_CANCELED]: (serviceName) =>
`Запрос к сервису ${serviceName} отменен пользователем`,
};
Список кодов и их значения можно найти в документации Axios.

Категория 3. Запрос не был отправлен, но объект ошибки является инстансом Axios
Если при попытке отправить запрос — не получается его отправить, в объекте ошибки будут отсутствовать свойства request и response. Например, при указании в url неподдерживаемого протокола:
import axios from 'axios';
axios
.get('htt://somewebsite.com')
.then(/*...*/)=>{
/*...*/
})
.catch((error) => {
console.error(error.message); // Unsupported protocol htt:
console.error(error.response); // undefined
console.error(error.request); // undefined
});
Тогда можно вернуть следующее сообщение:
`Не удалось отправить запрос к сервису ${serviceName}: ${error.message}`
И у нас остается последний вариант возникновения исключения при обмене данными между клиентом и сервером.
Категория 4. Ошибка не является инстансом AxiosError
Данная ситуация возможна, например, при передаче объекта с циклической ссылкой в качестве аргументов POST запроса. При этом ошибка будет инстансом системного класса Error или класса, производного от него:
import axios from 'axios';
const invalidData = { circularLink: {} };
// Создаем циклическую ссылку
invalidData.circularLink = invalidData;
axios
.post('http://somewebsite.com', invalidData)
.then(/*...*/)=>{
/*...*/
})
.catch((error) => {
console.error(error instanceof Error); // true
console.error(error instanceof RangeError); // true
console.error(error.message); // Maximum call stack size exceeded
console.error(error.response); // undefined
console.error(error.request); // undefined
})
;
Помимо этого, ошибка может иметь тип, отличный от системного Error, если в reject-колбэк промиса передали аргумент любого типа. Например, при перехвате запроса с помощью axios.interceptors.response.use:
import axios, { HttpStatusCode } from 'axios';
// Добавляем перехват ответов
axios.interceptors.response.use(
(response: any) => {
/*...*/
},
/* Любые коды состояния, выходящие за пределы диапазона 2xx,
вызывают срабатывание этой функции до .catch*/
(error: any) => {
if (error?.response?.status === HttpStatusCode.Unauthorized) {
return Promise.reject('Необходимо залогиниться в приложении');
}
return Promise.reject(error);
});
Когда ошибка не является инстансом AxiosError, это означает, что она произошла в клиентском коде. Возможно, до отправки запроса или после получения ответа от сервера. Посмотрим, как могут быть обработаны эти случаи на примере реализации метода _handleNotAxiosError:
private static _handleNotAxiosError(error: unknown): string[] {
// Сперва проверим не является ли ошибка инстансом системного класса Error
if (error instanceof Error) {
return [`Ошибка на стороне клиента: ${error.message}`];
}
/* Здесь может быть любой код, который получает сообщение об ошибке.
В том числе, если error является объектом, то можно попытаться достать
текст ошибки путем кастования к разным моделям ошибок. Для простоты здесь
указана только проверка на строковый тип */
return typeof error === 'string' ? [error] : ['Ошибка на стороне клиента'];
}
Опишем еще один полезный шаг в обработке ошибок запроса.
Постобработка сообщений об ошибках
Если в сообщениях об ошибках использовать названия сервисов и методов, то могут возникнуть ситуации, когда их не удастся определить. Тогда удалить лишние пробелы из сообщений можно следующим образом:
private static _processErrorMessages(messages: string[]): string[] {
const resultMessages: string[] = [];
messages.forEach((message) =>
resultMessages.push(message.replace(' ', ' ').trim()),
);
return resultMessages;
}
Если вы не используете в сообщениях названия методов и сервисов, то этот пункт можно пропустить.
Финальный вид метода обработки ошибок Axios
Мы рассмотрели 4 возможных случая, с которыми можно столкнуться при обработке ошибок запросов, сделанных с использованием библиотеки Axios. Каждая из них требует особого подхода для правильной интерпретации и обработки. Комплексный подход и представленные методы помогут улучшить пользовательский опыт и сделать показ ошибок более информативным.
Вот итоговый вид метода обработки ошибок HTTP-клиента Axios:
static async getErrors(error: any): Promise {
let errorMessages: string[];
if (axios.isAxiosError(error)) {
const serviceName = this._getServiceName(
error.config?.baseURL,
error.config?.url,
);
if (error?.response) {
errorMessages = await this._getResponseErrors(
error.response,
error.message,
serviceName,
);
} else if (error?.request) {
errorMessages = this._getRequestErrors(serviceName, error.code);
} else {
errorMessages = [
`Не удалось отправить запрос к сервису ${serviceName}: ${error.message}`,
];
}
} else {
errorMessages = this._handleNotAxiosError(error);
}
return this._processErrorMessages(errorMessages);
}
Предложенный алгоритм обработки исключений можно легко адаптировать под конкретные нужды вашего проекта. Пишите в комментариях свои мысли по поводу обработки ошибок запроса данных и задавайте вопросы. Буду рада на них ответить!
Надеюсь, что представленный материал поможет вам лучше понять механизмы обработки ошибок обмена данными с сервером, сделанных при помощи Axios, и применить их на практике для создания более удобных для пользователей приложений.