Обработка ошибок в TypeScript: от try/catch к надежному коду
TypeScript добавляет статическую типизацию в JavaScript, но ошибки времени выполнения остаются неотъемлемой частью разработки. Неправильная обработка исключений может привести к падению приложения, утечке данных или некорректной работе пользовательского интерфейса. В этой статье мы разберем, как эффективно обрабатывать ошибки в TypeScript, используя встроенные механизмы и продвинутые паттерны.
Базовый try/catch в TypeScript
Конструкция try/catch — основа обработки ошибок. В TypeScript, в отличие от JavaScript, мы можем типизировать перехваченную ошибку. Однако есть важный нюанс: по умолчанию тип ошибки в блоке catch — unknown, а не Error.
try {
const data = JSON.parse('невалидный json');
console.log(data);
} catch (error) {
// error имеет тип unknown
if (error instanceof Error) {
console.error('Произошла ошибка:', error.message);
} else {
console.error('Неизвестная ошибка:', error);
}
}
Почему это важно? В JavaScript можно выбросить что угодно: строку, число, объект. TypeScript защищает нас от небезопасного доступа к свойствам ошибки без проверки. Рекомендуется всегда проверять тип ошибки перед использованием.
Пользовательские классы ошибок
Для более точной обработки создавайте собственные классы ошибок, расширяющие встроенный Error. Это позволяет различать типы ошибок и передавать дополнительную информацию.
class ValidationError extends Error {
constructor(
public field: string,
message: string
) {
super(message);
this.name = 'ValidationError';
}
}
class NetworkError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = 'NetworkError';
}
}
function processUserData(data: unknown): void {
if (!data || typeof data !== 'object') {
throw new ValidationError('data', 'Некорректные данные');
}
if (!('email' in data)) {
throw new ValidationError('email', 'Email обязателен');
}
}
try {
processUserData({ name: 'Иван' });
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Ошибка валидации поля ${error.field}: ${error.message}`);
} else if (error instanceof NetworkError) {
console.error(`Сетевая ошибка ${error.statusCode}: ${error.message}`);
} else {
console.error('Неизвестная ошибка:', error);
}
}
Такой подход делает код самодокументируемым и упрощает отладку. Вы всегда знаете, где и почему произошла ошибка.
Типизация функций с ошибками: never и discriminated unions
TypeScript предоставляет мощные инструменты для работы с ошибками на уровне типов. Используйте тип never для функций, которые всегда выбрасывают исключение, и discriminated unions для возврата ошибок.
function throwError(message: string): never {
throw new Error(message);
}
// Discriminated union для результата
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function fetchUser(id: number): Result<{ name: string }> {
try {
// Имитация запроса
if (id <= 0) {
return { success: false, error: 'Некорректный ID' };
}
return { success: true, data: { name: 'Анна' } };
} catch (error) {
return { success: false, error: 'Ошибка сети' };
}
}
const result = fetchUser(1);
if (result.success) {
console.log('Пользователь:', result.data.name);
} else {
console.error('Ошибка:', result.error);
}
Паттерн Result позволяет избежать неявных исключений и делает обработку ошибок обязательной — TypeScript заставит вас проверить поле success перед доступом к данным.
Продвинутые паттерны: Either и Try
В функциональном программировании популярны монады Either и Try, которые можно реализовать на TypeScript. Они предоставляют элегантный способ цепочечной обработки ошибок.
class Either<L, R> {
private constructor(
private left: L | null,
private right: R | null
) {}
static left<L, R>(value: L): Either<L, R> {
return new Either(value, null);
}
static right<L, R>(value: R): Either<L, R> {
return new Either(null, value);
}
isLeft(): boolean {
return this.left !== null;
}
isRight(): boolean {
return this.right !== null;
}
map<U>(fn: (value: R) => U): Either<L, U> {
if (this.isRight()) {
return Either.right(fn(this.right!));
}
return Either.left(this.left!);
}
fold<T>(leftFn: (left: L) => T, rightFn: (right: R) => T): T {
if (this.isLeft()) {
return leftFn(this.left!);
}
return rightFn(this.right!);
}
}
function divide(a: number, b: number): Either<string, number> {
if (b === 0) {
return Either.left('Деление на ноль');
}
return Either.right(a / b);
}
const result2 = divide(10, 2)
.map(x => x * 2)
.fold(
error => console.error('Ошибка:', error),
value => console.log('Результат:', value)
);
Этот паттерн особенно полезен в сложных цепочках преобразований, где каждая операция может завершиться ошибкой.
Асинхронные ошибки и глобальный обработчик
В асинхронном коде ошибки нужно обрабатывать особенно внимательно. Не забывайте про catch у промисов и используйте глобальные обработчики для непредвиденных ситуаций.
// Глобальный обработчик неперехваченных ошибок
window.addEventListener('unhandledrejection', (event) => {
console.error('Необработанный промис:', event.reason);
// Отправка ошибки на сервер
});
// Обработка асинхронных функций
async function fetchData(url: string): Promise<unknown> {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(
response.status,
`HTTP ошибка: ${response.statusText}`
);
}
return response.json();
}
// Использование с try/catch
async function loadUserProfile(userId: number) {
try {
const data = await fetchData(`/api/users/${userId}`);
console.log('Профиль загружен:', data);
} catch (error) {
if (error instanceof NetworkError) {
showErrorMessage('Сетевая ошибка,