Обработка ошибок является фундаментальным аспектом программирования, который обеспечивает гармоничную обработку непредвиденных ситуаций. В JavaScript, хотя try-catch
часто используется, существуют более продвинутые техники для улучшения обработки ошибок.
В этой статье рассматриваются эти продвинутые методы, предоставляя практические решения для улучшения стратегий управления ошибками и делая ваши приложения более надежными.
Что такое обработка ошибок?
Цель обработки ошибок
Обработка ошибок предвидит, обнаруживает и реагирует на проблемы, возникающие во время выполнения программы. Правильная обработка ошибок улучшает пользовательский опыт, поддерживает стабильность приложения и обеспечивает надежность.
Типы ошибок в JavaScript
- Синтаксические ошибки. Это ошибки в синтаксисе кода, такие как отсутствие скобок или неправильное использование ключевых слов.
- Ошибки времени выполнения. Происходят во время выполнения, например, при доступе к свойствам неопределенных объектов.
- Логические ошибки. Эти ошибки не вызывают сбоев программы, но приводят к неправильным результатам, часто из-за недочетов в логике или непреднамеренных побочных эффектов.
Почему try-catch недостаточно
Ограничения try-catch
- Ограничения области видимости. Обрабатывает только синхронный код в своем блоке и не влияет на асинхронные операции, если они специально не обработаны.
- Тихие сбои. Чрезмерное использование или неправильное применение может привести к тому, что ошибки будут молча игнорироваться, что потенциально может вызвать непредвиденное поведение.
- Распространение ошибок. Не поддерживает естественное распространение ошибок через различные уровни приложения.
Когда использовать try-catch
- Синхронный код. Эффективен для обработки ошибок в синхронных операциях, таких как разбор JSON.
- Критические секции. Используйте для защиты критических участков кода, где ошибки могут иметь серьезные последствия.
Пользовательские классы ошибок: Улучшение информации об ошибке
Создание пользовательского класса ошибок
Пользовательские классы ошибок расширяют встроенный класс Error
для предоставления дополнительной информации:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.stack = (new Error()).stack; // Capture the stack trace
}
}
Преимущества пользовательских ошибок
- Ясность. Предлагает конкретные сообщения об ошибках.
- Детальная обработка. Позволяет обрабатывать отдельные типы ошибок по-разному.
- Метаданные об ошибке. Включает дополнительный контекст об ошибке.
Сценарии использования пользовательских ошибок
- Сбои валидации. Ошибки, связанные с проверкой пользовательского ввода.
- Ошибки, специфичные для домена. Ошибки, адаптированные под конкретные области применения, такие как аутентификация или обработка платежей.
Централизованная обработка ошибок
Глобальная обработка ошибок в Node.js
Централизуйте обработку ошибок, используя промежуточное программное обеспечение:
app.use((err, req, res, next) => {
console.error('Global error handler:', err);
res.status(500).json({ message: 'An error occurred' });
});
Централизованная обработка ошибок в веб-приложениях.
Реализуйте централизованную обработку ошибок в React с использованием границ ошибок:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Преимущества централизованной обработки ошибок
- Согласованность. Обеспечивает единый подход к управлению ошибками.
- Более простое обслуживание. Централизованные обновления снижают риск упущения изменений.
- Лучшее ведение журналов и мониторинг. Облегчает интеграцию с инструментами мониторинга.
Распространение ошибок
Распространение ошибок в синхронном коде
Используйте throw
для передачи ошибок:
function processData(data) {
try {
validateData(data);
saveData(data);
} catch (error) {
console.error('Failed to process data:', error);
throw error;
}
}
Распространение ошибок в асинхронном коде
Обработка ошибок с помощью промисов или async
/await
:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
Когда следует распространять ошибки
- Критические ошибки. Распространяйте ошибки, которые влияют на всё приложение.
- Бизнес-логика. Позвольте компонентам более высокого уровня обрабатывать ошибки бизнес-логики.
Обработка ошибок в асинхронном коде
Обработка ошибок с async/await
Используйте try-catch
для управления ошибками в асинхронных функциях:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return await response.json();
} catch (error) {
console.error('Error fetching user data:', error);
return null;
}
}
Использование Promise.all с обработкой ошибок
Обработка нескольких промисов и ошибок:
async function fetchMultipleData(urls) {
try {
const responses = await Promise.all(urls.map
(url => fetch(url)));
return await Promise.all(responses.map(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${response.url}`);
}
return response.json();
}));
} catch (error) {
console.error('Error fetching multiple data:', error);
return [];
}
}
Часто встречающиеся проблемы в обработке ошибок в асинхронном коде
- Необработанные промисы. Всегда обрабатывайте промисы с помощью
await
,.then()
или.catch()
. - Тихие сбои. Убедитесь, что ошибки не пропускаются молча.
- Гонки состояний. Будьте осторожны с параллельными асинхронными операциями.
Журналирование ошибок
Журналирование ошибок на стороне клиента
Захват глобальных ошибок:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global error captured:', message, source, lineno, colno, error);
sendErrorToService({ message, source, lineno, colno, error });
};
Журналирование ошибок на стороне сервера
Используйте инструменты вроде Winston для журналирования на сервере:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'error.log' })]
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
Мониторинг и оповещение
Настройте мониторинг в реальном времени и оповещения с помощью сервисов вроде PagerDuty или Slack:
function notifyError(error) {
// Send error details to monitoring service
}
Лучшие практики журналирования ошибок
- Включите контекст. Журналируйте дополнительный контекст, такой как данные запроса и информацию о пользователе.
- Избегайте избыточного журналирования. Журналируйте основную информацию, чтобы избежать проблем с производительностью.
- Регулярно анализируйте журналы. Регулярно проверяйте журналы, чтобы обнаружить и устранить повторяющиеся проблемы.
Грациозное снижение и резервные варианты
Грациозное снижение
Спроектируйте ваше приложение так, чтобы оно продолжало работать с ограниченными возможностями:
function renderProfilePicture(user) {
try {
if (!user.profilePicture) {
throw new Error('Profile picture not available');
}
return `<img data-fr-src="${user.profilePicture}" alt="Profile Picture">`;
} catch (error) {
console.error('Error rendering profile picture:', error.message);
return '<img src="/default-profile.png" alt="Default Profile Picture">';
}
}
Механизмы резервирования
Предоставьте альтернативы, когда основные операции не удаются:
async function fetchDataWithFallback(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
return { message: 'Default data' }; // Fallback data
}
}
Реализация грациозного снижения
- Резервные варианты пользовательского интерфейса. Предоставьте альтернативные элементы пользовательского интерфейса, когда функции не работают.
- Резервные варианты данных. Используйте закешированные или значения по умолчанию, когда живые данные недоступны.
- Механизмы повторной попытки. Реализуйте логику повторной попытки для временных ошибок.
Балансировка грациозного снижения
Сбалансируйте предоставление резервных вариантов с информированием пользователей о проблемах:
function showErrorNotification(message) {
// Notify users about the issue
}
Тестирование обработки ошибок
Тестирование обработки ошибок в модулях
Проверка обработки ошибок в отдельных функциях:
const { validateUserInput } = require('./validation');
test('throws error for invalid username', () => {
expect(() => {
validateUserInput({ username: 'ab' });
}).toThrow('Username must be at least 3 characters long.');
});
Интеграционное тестирование
Тестирование обработки ошибок на разных уровнях приложения:
test('fetches data with fallback on error', async () => {
fetch.mockReject(new Error('Network error'));
const data = await fetchDataWithFallback('https://api.example.com/data');
expect(data).toEqual({ message: 'Default data' });
});
Тестирование end-to-end
Симуляция сценариев реального мира для тестирования обработки ошибок:
describe('ErrorBoundary', () => {
it('displays error message on error', () => {
cy.mount(<ErrorBoundary><MyComponent /></ErrorBoundary>);
cy.get(MyComponent).then(component => {
component.simulateError(new Error('Test error'));
});
cy.contains('Something went wrong.').should('be.visible');
});
});
Лучшие практики тестирования обработки ошибок
- Рассмотрите крайние случаи. Убедитесь, что тесты учитывают различные сценарии ошибок.
- Тестирование резервных вариантов. Проверьте, что механизмы резервирования работают как задумано.
- Автоматизируйте тестирование. Используйте CI/CD пайплайны для автоматизации и обеспечения надежной обработки ошибок.
Сценарии из реальной жизни
Сценарий 1: Система обработки платежей
Обработка ошибок во время обработки платежей:
- Пользовательские классы ошибок. Используйте классы типа
CardValidationError
,PaymentGatewayError
. - Логика повторных попыток. Внедрите повторные попытки для проблем, связанных с сетью.
- Централизованное ведение журналов. Отслеживайте ошибки платежей и оперативно решайте проблемы.
Сценарий 2: Приложения с обработкой данных
Управление ошибками при обработке данных:
- Грациозное снижение. Предоставляйте частичные данные или альтернативные представления.
- Резервные данные. Используйте кэшированные или значения по умолчанию.
- Журналирование ошибок. Регистрация подробного контекста для устранения неполадок.
Сценарий 3: Аутентификация и авторизация пользователя
Обработка ошибок аутентификации и авторизации:
- Пользовательские классы ошибок. Создание классов, таких как
AuthenticationError
,AuthorizationError
. - Централизованная обработка. Регистрация и мониторинг проблем, связанных с аутентификацией.
- Гармоничное снижение. Предложение альтернативных вариантов входа и информативных сообщений об ошибках.
Заключение
Расширенная обработка ошибок в JavaScript требует отхода от простого использования try-catch
и принятия пользовательских ошибок, централизованной обработки, распространения и надежного тестирования. Применение этих техник позволяет создавать надежные приложения, обеспечивающие беспрепятственный пользовательский опыт, даже когда возникают проблемы.
Дополнительное чтение
- “JavaScript: The Good Parts” Дуглас Крокфорд
- “You Don’t Know JS: Async & Performance” Кайл Симпсон
- MDN Web Docs: Обработка ошибок
Source:
https://dzone.com/articles/advanced-error-handling-in-javascript-custom-error