Надёжная обработка ошибок - основа безотказных приложений на TypeScript.
Это руководство охватывает всё: от базовых конструкций try/catch до продвинутых паттернов обработки ошибок.
Базовая обработка ошибок
Блоки try/catch
Основа обработки ошибок в TypeScript:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Деление на ноль');
}
return a / b;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error('Произошла ошибка:', error.message);
}
Примечание для TypeScript 4.0+
В TypeScript 4.0 и новее тип unknown — тип по умолчанию для переменных в блоке catch. Всегда сужайте тип перед обращением к свойствам.
Пользовательские классы ошибок
Создание пользовательских классов ошибок
Расширьте встроенный класс Error, чтобы обрабатывать свои специфичные ошибки:
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
// Восстанавливаем цепочку прототипов
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
class DatabaseError extends Error {
constructor(message: string, public code: number) {
super(message);
this.name = 'DatabaseError';
Object.setPrototypeOf(this, DatabaseError.prototype);
}
}
// Использование
function validateUser(user: any) {
if (!user.name) {
throw new ValidationError('Имя обязательно', 'name');
}
if (!user.email.includes('@')) {
throw new ValidationError('Неверный формат email', 'email');
}
}
Защитники типов для ошибок
Предикаты типов для обработки ошибок
Создайте защитники типов, чтобы безопасно работать с разными типами ошибок:
// Защитники типов
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
// Использование в блоке catch
try {
validateUser({});
} catch (error: unknown) {
if (isValidationError(error)) {
console.error(`Ошибка валидации в поле ${error.field}: ${error.message}`);
} else if (isErrorWithMessage(error)) {
console.error('Произошла ошибка:', error.message);
} else {
console.error('Произошла неизвестная ошибка');
}
}
Паттерн утверждения типа
Для более сложной обработки ошибок можно использовать функции утверждения типа:
function assertIsError(error: unknown): asserts error is Error {
if (!(error instanceof Error)) {
throw new Error('Перехваченное значение — не экземпляр Error');
}
}
try {
// ...
} catch (error) {
assertIsError(error);
console.error((error as Error).message); // TypeScript теперь знает, что error — это Error
}
Обработка ошибок в асинхронном коде
Обработка ошибок в async/await
Правильная обработка ошибок в коде с async/await требует оборачивания вызовов await в блоки try/catch:
interface User {
id: number;
name: string;
email: string;
}
// Использование async/await с try/catch
async function fetchUser(userId: number): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP‑ошибка! Статус: ${response.status}`);
}
return await response.json() as User;
} catch (error) {
if (error instanceof Error) {
console.error('Не удалось загрузить пользователя:', error.message);
}
throw error; // Повторно выбрасываем, чтобы вызывающий код мог обработать
}
}
// Использование Promise.catch() для обработки ошибок
function fetchUserPosts(userId: number): Promise<Post[]> {
return fetch(`/api/users/${userId}/posts`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP‑ошибка! Статус: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Не удалось загрузить посты:', error);
return []; // Возвращаем пустой массив как запасной вариант
});
}
Необработанные отклонения промисов
Всегда обрабатывайте отклонения промисов, чтобы избежать предупреждений о необработанных отклонениях:
// Плохо: необработанное отклонение промиса
fetchData().then(data => console.log(data));
// Хорошо: обрабатываем и успех, и ошибку
fetchData()
.then(data => console.log('Успех:', data))
.catch(error => console.error('Ошибка:', error));
// Или используем void для намеренно игнорируемых ошибок
void fetchData().catch(console.error);
Компоненты-перехватчики ошибок в React
Компонент-перехватчик ошибок в React
Создавайте компонент-перехватчик ошибок или границу ошибок, чтобы перехватывать JavaScript‑ошибки в деревьях компонентов React:
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = {
hasError: false
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Необработанная ошибка:', error, errorInfo);
// Отправляем в сервис отчётов об ошибках
}
public render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Что‑то пошло не так</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Попробовать снова
</button>
</div>
);
}
return this.props.children;
}
}
// Использование
function App() {
return (
<ErrorBoundary fallback={<div>Ой! Что‑то сломалось.</div>}>
<MyComponent />
</ErrorBoundary>
);
}
Лучшие практики
Всегда обрабатывайте ошибки
Никогда не оставляйте блоки catch пустыми. Как минимум, логируйте ошибку:
// Плохо: молчаливый сбой
try { /* ... */ } catch { /* пусто */ }
// Хорошо: как минимум логируем ошибку
try { /* ... */ } catch (error) {
console.error('Операция не удалась:', error);
}
Используйте специфические типы ошибок
Создавайте пользовательские классы ошибок для разных сценариев сбоев:
class NetworkError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
Обрабатывайте ошибки на правильном уровне
Обрабатывайте ошибки там, где у вас достаточно контекста, чтобы восстановить работу или обеспечить хороший пользовательский опыт:
// В слое доступа к данным
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NetworkError(response.status, 'Не удалось загрузить пользователя');
}
return response.json();
}
// В UI‑компоненте
async function loadUser() {
try {
const user = await getUser('123');
setUser(user);
} catch (error) {
if (error instanceof NetworkError) {
if (error.status === 404) {
showError('Пользователь не найден');
} else {
showError('Сетевая ошибка. Пожалуйста, попробуйте позже.');
}
} else {
showError('Произошла непредвиденная ошибка');
}
}
}
Типичные ошибки
Необработанные отклонения промисов
Всегда обрабатывайте отклонения промисов, чтобы избежать предупреждений о необработанных отклонениях:
// Плохо: необработанное отклонение промиса
fetchData();
// Хорошо: обрабатываем отклонение
fetchData().catch(console.error);
Перехват ошибок без сужения типа
В TypeScript 4.0+ пойманные ошибки имеют тип unknown:
// Плохо: ошибка имеет тип 'unknown'
try { /* ... */ } catch (error) {
console.log(error.message); // Ошибка: свойство 'message' не существует в типе 'unknown'
}
// Хорошо: сужаем тип
try { /* ... */ } catch (error) {
if (error instanceof Error) {
console.log(error.message); // OK
}
}
Замалчивание ошибок
Избегайте молчаливого перехвата и игнорирования ошибок без должной обработки:
// Плохо: ошибка молча игнорируется
function saveData(data: Data) {
try {
database.save(data);
} catch {
// Игнорировать
}
}
// Лучше: логируем ошибку и/или уведомляем пользователя
function saveData(data: Data) {
try {
database.save(data);
} catch (error) {
console.error('Не удалось сохранить данные:', error);
showError('Не удалось сохранить данные. Пожалуйста, попробуйте ещё раз.');
}
}
Итоги
Эффективная обработка ошибок в TypeScript включает:
- использование блоков
try/catchдля синхронного кода; - обработку отклонений промисов с помощью
.catch()илиtry/catchв сочетании сasync/await; - создание пользовательских классов ошибок для специфичных ошибок;
- использование защитников типов для безопасной работы с объектами ошибок;
- обработку ошибок на соответствующем уровне в вашем приложении;
- предоставление понятных сообщений об ошибках для пользователей.
Следуя этим практикам, вы сможете создавать более надёжные и удобные в сопровождении приложения на TypeScript.