Обработка ошибок в TypeScript
Хорошая обработка ошибок необходима для создания надёжных приложений на TypeScript.
Эта страница проходит путь от базового блока try/catch до более продвинутых приёмов: собственные классы ошибок, защитники типов, асинхронные ошибки и лучшие практики.
Базовая обработка ошибок
Код, который может завершиться ошибкой, помещается внутрь блока try. Если там что-то выбрасывается, выполнение переходит в блок catch, где вы получаете выброшенное значение.
Примечание: в TypeScript 4.0 и новее (при включённой опции useUnknownInCatchVariables, которую включает strict) переменная в catch имеет тип unknown. Всегда сужайте её тип, прежде чем читать такие свойства, как message.
Блоки try/catch
Основа обработки ошибок в TypeScript: divide выбрасывает ошибку при делении на ноль, а блок catch сообщает о ней.
divide(a: number, b: number): number, которая выбрасывает new Error("Деление на ноль"), когда b равно 0. Вызовите divide(10, 0) внутри блока try, который вывел бы результат, а в catch выведите Произошла ошибка: и следом сообщение ошибки с помощью console.error.
Вывод: программа на 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);
}
Собственные классы ошибок
Вы можете расширить встроенный класс Error, чтобы создавать ошибки, которые несут дополнительную информацию о вашей предметной области, например поле, не прошедшее проверку, или код ошибки базы данных.
Создание собственных классов ошибок
ValidationError и DatabaseError расширяют Error, задают собственное значение name и восстанавливают цепочку прототипов, чтобы instanceof работал после компиляции в более старые версии стандарта.
Error.
Определите ValidationError (сообщение плюс необязательное публичное поле field) и DatabaseError (сообщение плюс публичное числовое поле code); оба задают this.name и восстанавливают цепочку прототипов с помощью Object.setPrototypeOf. Добавьте функцию validateUser(user: any), которая выбрасывает ValidationError при отсутствующем имени (поле name) или при email без @ (поле email). Только объявления — ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод
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');
}
}
Защитники типов для ошибок
Поскольку пойманное значение может быть чем угодно, пишите защитники типов — функции, возвращающие error is SomeType, — чтобы проверить, что вы на самом деле поймали, прежде чем это использовать.
Для более сложных случаев можно также использовать функцию-утверждение (asserts error is Error), показанную в сниппете ниже: после её вызова TypeScript считает значение типом Error.
Предикаты типов для обработки ошибок
Два защитника типов позволяют блоку catch обработать ValidationError, любой объект со строковым свойством message или совершенно неизвестное значение.
unknown.
Повторно используйте класс ValidationError (сообщение и необязательное поле field) и функцию validateUser, которая выбрасывает его, когда отсутствует name. Напишите isErrorWithMessage(error: unknown): error is { message: string } (объект, не null, есть строковое свойство message) и isValidationError(error: unknown): error is ValidationError с использованием instanceof. Вызовите validateUser({}) в блоке try; в catch (error: unknown) выведите Ошибка валидации в поле <field>: <message> для ошибок валидации, Произошла ошибка: <message> для других ошибок с сообщением или Произошла неизвестная ошибка.
Вывод: программа на TypeScript и захваченный вывод
class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
Object.setPrototypeOf(this, ValidationError.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('Произошла неизвестная ошибка');
}
}
Функция-утверждение для ошибок
assertIsError выбрасывает ошибку, если значение не является Error; иначе TypeScript сужает пойманное значение до Error до конца блока.
function assertIsError(error: unknown): asserts error is Error {
if (!(error instanceof Error)) {
throw new Error('Caught value is not an Error instance');
}
}
try {
// ...
} catch (error) {
assertIsError(error);
console.error((error as Error).message); // TypeScript now knows error is Error
}
Обработка асинхронных ошибок
При использовании async/await ошибки ожидаемых промисов перехватываются обычным try/catch. С обычными промисами вместо этого добавляйте в цепочку обработчик .catch().
Никогда не оставляйте отклонение промиса необработанным. Либо добавляйте .catch(), либо используйте void, чтобы показать, что результат игнорируется намеренно, но ошибки при этом всё равно перехватываются.
Обработка ошибок async/await
Обёртывание await в try/catch позволяет записать ошибку в лог и пробросить её дальше; цепочка промисов может восстановиться, вернув запасное значение в .catch(). Сетевой вызов заменён локальной заглушкой, чтобы пример работал где угодно.
.catch() у Promise.
Определите интерфейсы User и Post и локальную функцию mockFetch(url), которая возвращает уже выполненный поддельный ответ (пользователя John Doe для /api/users/1 и один пост с заголовком Привет, мир для URL с /posts), чтобы сеть не использовалась. Напишите fetchUser(userId): Promise<User>, которая ожидает ответ, выбрасывает Ошибка HTTP! статус: <status>, когда ответ не ok, выводит Не удалось загрузить пользователя: в своём блоке catch и пробрасывает ошибку дальше; и fetchUserPosts(userId): Promise<Post[]> на основе .then() с той же проверкой и с .catch(), который выводит Не удалось загрузить посты: и возвращает []. Вызовите обе функции и выведите имя пользователя и массив постов.
Вывод: программа на TypeScript и захваченный вывод
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
}
// Локальная замена fetch, чтобы пример работал без сети
function mockFetch(url: string): Promise<{ ok: boolean; status: number; json: () => Promise<any> }> {
if (url.endsWith('/posts')) {
return Promise.resolve({ ok: true, status: 200, json: async () => [{ id: 1, title: 'Привет, мир' }] });
}
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({ id: 1, name: 'John Doe', email: '[email protected]' })
});
}
// Использование async/await вместе с try/catch
async function fetchUser(userId: number): Promise<User> {
try {
const response = await mockFetch(`/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 mockFetch(`/api/users/${userId}/posts`)
.then(response => {
if (!response.ok) {
throw new Error(`Ошибка HTTP! статус: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('Не удалось загрузить посты:', error);
return []; // Возвращаем пустой массив как запасное значение
});
}
// Использование
fetchUser(1).then(user => console.log('Пользователь:', user.name));
fetchUserPosts(1).then(posts => console.log('Посты:', posts));
Обработка отклонений промисов
Промис без .catch() — это ошибка, которая ждёт своего часа; два хороших варианта обрабатывают отклонение или явно игнорируют результат с помощью void.
// Bad: Unhandled promise rejection
fetchData().then(data => console.log(data));
// Good: Handle both success and error cases
fetchData()
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
// Or use void for intentionally ignored errors
void fetchData().catch(console.error);
Границы ошибок в React
В приложении на React граница ошибок — это классовый компонент, который перехватывает ошибки JavaScript, выброшенные в любом месте дерева его дочерних компонентов, и показывает запасной интерфейс вместо падения всей страницы.
Компонент границы ошибок в React
Класс ErrorBoundary с типизированными пропсами и состоянием: он сохраняет ошибку в getDerivedStateFromError, записывает её в лог в componentDidCatch и отрисовывает запасной интерфейс.
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('Uncaught error:', error, errorInfo);
// Log to error reporting service
}
public render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary fallback={<div>Oops! Something broke.</div>}>
<MyComponent />
</ErrorBoundary>
);
}
Лучшие практики
Всегда обрабатывайте ошибки. Никогда не оставляйте блок catch пустым — как минимум записывайте ошибку в лог.
Используйте конкретные типы ошибок. Создавайте отдельный класс ошибки для каждого вида сбоя, чтобы вызывающий код мог их различать.
Обрабатывайте ошибки на правильном уровне. Перехватывайте ошибку там, где у вас достаточно контекста, чтобы восстановиться или показать пользователю понятное сообщение, — часто это не та функция, которая её выбрасывает.
Никогда не оставляйте catch пустым
Молчаливый catch скрывает сбои; запись ошибки в лог — это минимум.
// Bad: Silent failure
try { /* ... */ } catch { /* empty */ }
// Good: At least log the error
try { /* ... */ } catch (error) {
console.error('Operation failed:', error);
}
Конкретные классы ошибок
NetworkError несёт HTTP-статус, а ValidationError — имя поля, не прошедшего проверку.
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';
}
}
Обработка ошибок на правильном уровне
Слой доступа к данным выбрасывает NetworkError; слой интерфейса перехватывает его и решает, какое сообщение показать.
// In a data access layer
async function getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new NetworkError(response.status, 'Failed to fetch user');
}
return response.json();
}
// In a UI component
async function loadUser() {
try {
const user = await getUser('123');
setUser(user);
} catch (error) {
if (error instanceof NetworkError) {
if (error.status === 404) {
showError('User not found');
} else {
showError('Network error. Please try again later.');
}
} else {
showError('An unexpected error occurred');
}
}
}
Типичные ошибки
Необработанные отклонения промисов. Всегда добавляйте промисам обработчик, иначе вы получите предупреждения о необработанных отклонениях и потеряете ошибки.
Перехват без сужения типа. В TypeScript 4.0+ пойманное значение имеет тип unknown; чтение error.message без проверки — ошибка компиляции.
Проглатывание ошибок. Не перехватывайте ошибку только для того, чтобы её проигнорировать. Записывайте её в лог и, где это уместно, сообщайте пользователю.
Итог — эффективная обработка ошибок в TypeScript означает:
- Использование
try/catchдля синхронного кода - Обработку отклонений промисов через
.catch()или черезtry/catchвместе сasync/await - Создание собственных классов ошибок для ошибок предметной области
- Использование защитников типов для безопасной работы с объектами ошибок
- Обработку ошибок на правильном уровне приложения
- Понятные сообщения об ошибках для пользователей
Необработанное отклонение промиса
Вызов асинхронной функции с потерей промиса против добавления .catch().
// Bad: Unhandled promise rejection
fetchData();
// Good: Handle the rejection
fetchData().catch(console.error);
Перехват без сужения типа
Чтение message у ошибки типа unknown не работает; проверка instanceof Error делает это безопасным.
// Bad: Error is of type 'unknown'
try { /* ... */ } catch (error) {
console.log(error.message); // Error: Property 'message' does not exist on type 'unknown'
}
// Good: Narrow the type
try { /* ... */ } catch (error) {
if (error instanceof Error) {
console.log(error.message); // OK
}
}
Проглатывание ошибок
Первая версия saveData скрывает сбои; вторая записывает их в лог и уведомляет пользователя.
// Bad: Error is silently ignored
function saveData(data: Data) {
try {
database.save(data);
} catch {
// Ignore
}
}
// Better: Log the error and/or notify the user
function saveData(data: Data) {
try {
database.save(data);
} catch (error) {
console.error('Failed to save data:', error);
showError('Failed to save data. Please try again.');
}
}