AggregateError в JavaScript
AggregateError
помогает обрабатывать сразу несколько ошибок в JavaScript. Это упрощает управление кодом и делает его более надежным.Несколько дней назад я просматривал все типы ошибок в JavaScript и наткнулся на нечто относительно новое: AggregateError
Тип ошибки | Описание |
---|---|
Error | Базовый объект ошибки для общих ошибок. |
SyntaxError | Выбрасывается при наличии синтаксической ошибки в коде. |
ReferenceError | Выбрасывается при обращении к несуществующей переменной. |
TypeError | Выбрасывается, когда значение не соответствует ожидаемому типу. |
RangeError | Выбрасывается, когда число выходит за пределы допустимого диапазона. |
URIError | Выбрасывается при возникновении проблемы с кодированием/декодированием компонентов URI. |
EvalError | Исторический, использовался при неправильном использовании eval() (устарел). |
AggregateError | Выбрасывается, когда несколько ошибок должны быть обработаны вместе. (добавлено в ECMAScript 2021) |
AggregateError
был добавлен в ECMAScript 2021 (ES12). Он призван помочь в тех случаях, когда необходимо обработать сразу несколько ошибок. Это очень удобно в таких сценариях, как работа с промисами, когда требуется обработать все ошибки вместе, а не по одной.
Если используете Promise.any()
для поиска первого успешного промиса, но все они провалились, JavaScript выбросит ошибку AggregateError
. В этой ошибке перечислены все причины, по которым промисы не сработали.
const fetchFromApi1 = () => Promise.reject(new Error('API 1 failed'));
const fetchFromApi2 = () => Promise.reject(new Error('API 2 failed'));
const fetchFromApi3 = () => Promise.reject(new Error('API 3 failed'));
async function fetchWithRetry(apis, retries = 3, delay = 1000) {
try {
const data = await Promise.any(apis);
console.log('Data fetched:', data);
return data;
} catch (e) {
if (e instanceof AggregateError) {
console.log(e.name); // "AggregateError"
console.log(e.message); // "All promises were rejected"
console.log('Errors:', e.errors); // [Error: API 1 failed, Error: API 2 failed, Error: API 3 failed]
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithRetry(apis, retries - 1, delay);
} else {
console.log('All data sources failed after multiple attempts. Please try again later.');
throw e; // Повторный выброс ошибки после завершения повторных попыток
}
} else {
// Если ошибка не AggregateError, выбросьте её заново
throw e;
}
}
}
// Старт получения данных с повторными попытками
fetchWithRetry([fetchFromApi1(), fetchFromApi2(), fetchFromApi3()]);
Разбор кода
- Использую
Promise.any()
, чтобы попытаться получить данные из нескольких API и разрешить их с помощью первого успешного результата. Если все промисы оказываются неудачными, выбрасываетсяAggregateError
. - Обрабатываю потенциальные ошибки с помощью блока
try/catch
, где блокtry
работает с успешным получением данных, а блокcatch
ловитAggregateError
, если все промисы были отклонены. - Если возникает
AggregateError
, пишу в лог данные об ошибке, а затем пытаюсь повторить операцию до определённого количества раз с задержкой между каждым повтором. - Реализую логику повторных попыток, рекурсивно вызывая функцию, каждый раз уменьшая количество повторных попыток, пока либо не получу успешный результат, либо не исчерпаю все попытки.
- Если все повторные попытки не увенчались успехом, записываю в лог финальное сообщение об ошибке и повторно выбрасываю
AggregateError
, чтобы при необходимости её можно было обработать дальше.
AggregateError
упрощает обработку ошибок, когда имеете дело с несколькими сбоями одновременно, особенно в асинхронном коде. Это одна из тех функций, которые могут показаться ненужными, пока не столкнётесь с ситуацией, когда они избавят вас от головной боли.
Без AggregateError
Чтобы проиллюстрировать полезность AggregateError
, продемонстрирую, как обработка нескольких отклонённых промисов без него может быть громоздкой и менее эффективной. Вот как можно было бы справиться с этой ситуацией без AggregateError
const fetchFromApi1 = () => Promise.reject(new Error('API 1 failed'));
const fetchFromApi2 = () => Promise.reject(new Error('API 2 failed'));
const fetchFromApi3 = () => Promise.reject(new Error('API 3 failed'));
async function fetchWithoutAggregateError(apis, retries = 3, delay = 1000) {
try {
const data = await Promise.any(apis);
console.log('Data fetched:', data);
return data;
} catch (e) {
console.log('All promises failed.');
// Ручная обработка каждого отклонённого промиса путём проверки того, какие промисы не сработали
for (let i = 0; i < apis.length; i++) {
try {
await apis[i];
} catch (error) {
console.log(`Error from API ${i + 1}:`, error.message);
}
}
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
await new Promise(resolve => setTimeout(resolve, delay));
return fetchWithoutAggregateError(apis, retries - 1, delay);
} else {
console.log('All data sources failed after multiple attempts. Please try again later.');
throw new Error('All data sources failed after retries.');
}
}
}
// Старт получения данных с повторными попытками
fetchWithoutAggregateError([fetchFromApi1(), fetchFromApi2(), fetchFromApi3()]);
Когда Promise.any()
отклоняется, у нас нет AggregateError
, чтобы быстро получить доступ ко всем ошибкам. Вместо этого нужно пройтись по исходным промисам по отдельности проверить, какие из них не сработали, повторив попытку снова, и регистрируя каждый сбой.
function validateUserWithoutAggregateError(user) {
if (!user.name) {
throw new Error('Name is required');
}
if (!user.email) {
throw new Error('Email is required');
}
if (user.age < 18) {
throw new Error('User must be at least 18 years old');
}
return true;
}
try {
validateUserWithoutAggregateError({ name: '', email: '', age: 17 });
} catch (e) {
console.log(e.message); // Только первая ошибка будет поймана и показана
// Output: "Name is required"
}
Если не использовать AggregateError
, вам придётся выбрасывать и обрабатывать каждую ошибку по отдельности, что усложняет логику обработки ошибок.
Проблемы возникающие без AggregateError
- Ограниченная обратная связь: Функция останавливается на первой ошибке, поэтому пользователь видит только одно сообщение об ошибке за раз. Это может расстраивать пользователей, поскольку им приходится исправлять одну проблему и снова отправлять форму, чтобы увидеть следующую ошибку.
- Громоздкий код: Для обработки каждой ошибки по отдельности требуется повторяющийся код, если необходимо собрать и показать все ошибки сразу.
С AggregateError
С AggregateError
обработка ошибок централизована и упрощена. Он автоматически собирает все отказы, позволяя управлять ими более структурировано, группируя несколько ошибок вместо того, чтобы разбираться с каждой по отдельности.
function validateUser(user) {
let errors = [];
if (!user.name) {
errors.push(new Error('Name is required'));
}
if (!user.email) {
errors.push(new Error('Email is required'));
}
if (user.age < 18) {
errors.push(new Error('User must be at least 18 years old'));
}
if (errors.length > 0) {
throw new AggregateError(errors, 'Validation failed');
}
return true;
}
try {
validateUser({ name: '', email: '', age: 17 });
} catch (e) {
if (e instanceof AggregateError) {
console.log(e.name); // "AggregateError"
console.log(e.message); // "Validation failed"
e.errors.forEach(err => console.log(err.message));
// Output:
// "Name is required"
// "Email is required"
// "User must be at least 18 years old"
}
}
Преимущества использования AggregateError
- Консолидированный отчёт об ошибках: Все ошибки валидации собираются и передаются вместе. Пользователь получает информацию обо всех проблемах сразу, что улучшает пользовательский опыт.
- Более простая обработка ошибок: Вместо того чтобы обрабатывать множество отдельных ошибок, понадобится всего один блок
try/catch
для обработки всех ошибок. - Полная отладочная информация:
AggregateError
содержит список всех ошибок, что облегчает отладку и позволяет понять, что именно пошло не так.
Использование AggregateError
для группировки нескольких ошибок имеет ряд преимуществ. Это особенно полезно, когда сразу несколько вещей могут пойти не так, как надо, например при проверке формы или параллельных запросах API.