AggregateError в JavaScript

Источник: «AggregateError in 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()]);

Разбор кода

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

Использование AggregateError для группировки нескольких ошибок имеет ряд преимуществ. Это особенно полезно, когда сразу несколько вещей могут пойти не так, как надо, например при проверке формы или параллельных запросах API.

Дополнительные материалы

Предыдущая Статья

Artisan: Инструмент управления Laravel

Следующая Статья

Фоны для блочной модели (и чем она может быть полезна)