49. Error Handling в TypeScript
TypeScript: Обработка Ошибок (Error Handling) как Профи
Заголовок раздела «TypeScript: Обработка Ошибок (Error Handling) как Профи»Привет, кодер! С тобой Яша, и сегодня мы погрузимся в одну из самых критичных, но часто недооцененных тем в разработке — обработку ошибок. В мире, где код взаимодействует с внешними API, пользовательским вводом и непредсказуемыми условиями, ошибки — это не баги, а скорее неизбежные спутники. И наш долг — встречать их во всеоружии!
TypeScript, с его мощной системой типов, дает нам уникальные инструменты для того, чтобы делать обработку ошибок не просто функциональной, но и типобезопасной. Забудь о неопределенных any в блоках catch; пришло время обрабатывать ошибки так же элегантно, как и любой другой тип данных.
В этом уроке мы разберем:
- Как
try...catchработает сunknownв TypeScript. - Как создавать свои, выразительные типы ошибок.
- Продвинутые паттерны, такие как
Resultтип, для явного управления ошибками. - Типичные подводные камни и как их обходить.
Готов? Поехали!
🛡️ Основы: try…catch и Типизация Ошибок
Заголовок раздела «🛡️ Основы: try…catch и Типизация Ошибок»В JavaScript, блок catch традиционно ловит что угодно, что было выброшено (thrown). До TypeScript 4.0 переменная в catch блоке по умолчанию имела тип any. Это было удобно, но опасно, ведь мы теряли всякую типобезопасность.
Начиная с TypeScript 4.0 (и с флагом useUnknownInCatchVariables или strict mode), переменная в catch блоке по умолчанию имеет тип unknown. Это гораздо безопаснее! Почему? Потому что unknown заставляет нас явно проверять тип пойманной ошибки, прежде чем мы сможем что-то с ней сделать. Это как если бы тебе пришла посылка без этикетки – ты же не будешь ее сразу открывать и есть, не зная, что внутри, верно? Ты сначала ее проверишь!
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: unknown) { // error теперь имеет тип unknown if (error instanceof Error) { // Мы уверены, что это объект Error, и можем безопасно получить доступ к его свойствам console.error(`Ошибка при делении: ${error.message}`); } else { // Это что-то совершенно другое, не объект Error console.error("Произошла неизвестная ошибка:", error); }}
// Пример с другим типом выброшенного значения (TypeScript все равно поймает как unknown)function riskyOperation(shouldThrowString: boolean): void { if (shouldThrowString) { throw "Что-то пошло не так как строка!"; // Можем выбросить что угодно } console.log("Операция завершена успешно.");}
try { riskyOperation(true);} catch (error: unknown) { if (typeof error === 'string') { console.error(`Поймана строка-ошибка: "${error}"`); } else if (error instanceof Error) { console.error(`Поймана ошибка Error: ${error.message}`); } else { console.error("Неизвестный тип ошибки:", error); }}Использование unknown и instanceof или typeof для сужения типа ошибки — это золотой стандарт в современном TypeScript.
💥 Создаем Свои Ошибки: Когда Error Мало
Заголовок раздела «💥 Создаем Свои Ошибки: Когда Error Мало»Стандартный Error хорош, но часто нам нужно больше контекста. Например, ошибка сети отличается от ошибки валидации данных. Создание собственных классов ошибок позволяет нам:
- Добавить специфичные данные: Код ошибки, статус HTTP, поле, вызвавшее ошибку.
- Улучшить читаемость: Имя класса сразу говорит о сути проблемы.
- Упростить обработку: Мы можем ловить и обрабатывать конкретные типы ошибок.
Всегда наследуйтесь от Error или другого класса ошибок (например, TypeError, RangeError), чтобы сохранить стандартное поведение (stack trace и т.д.).
// Определяем собственный тип для кода ошибкиenum ErrorCode { ValidationFailed = 'VALIDATION_FAILED', NetworkError = 'NETWORK_ERROR', Unauthorized = 'UNAUTHORIZED',}
// 1. Ошибка валидации данныхclass ValidationError extends Error { public readonly code: ErrorCode = ErrorCode.ValidationFailed; public readonly field?: string;
constructor(message: string, field?: string) { super(message); // Вызываем конструктор базового класса Error this.name = 'ValidationError'; // Устанавливаем имя для идентификации this.field = field;
// Важно для корректного stack trace в некоторых окружениях Object.setPrototypeOf(this, ValidationError.prototype); }}
// 2. Ошибка сетевого взаимодействияclass NetworkError extends Error { public readonly code: ErrorCode = ErrorCode.NetworkError; public readonly statusCode?: number;
constructor(message: string, statusCode?: number) { super(message); this.name = 'NetworkError'; this.statusCode = statusCode; Object.setPrototypeOf(this, NetworkError.prototype); }}
// Пример использованияfunction validateUserData(username: string, email: string): void { if (username.length < 3) { throw new ValidationError("Имя пользователя должно быть не менее 3 символов", "username"); } if (!email.includes('@')) { throw new ValidationError("Некорректный формат email", "email"); } console.log("Данные пользователя валидны.");}
async function fetchData(url: string): Promise<string> { try { // Имитируем сетевой запрос const response = await new Promise<Response>((resolve, reject) => { setTimeout(() => { if (url.includes('error')) { reject(new Response(null, { status: 500, statusText: 'Internal Server Error' })); } else if (url.includes('unauthorized')) { reject(new Response(null, { status: 401, statusText: 'Unauthorized' })); } else { resolve(new Response('{"data": "some-data"}', { status: 200 })); } }, 500); });
if (!response.ok) { // Если ответ не OK, выбрасываем NetworkError throw new NetworkError(`Ошибка HTTP: ${response.statusText}`, response.status); } return await response.text(); } catch (err: unknown) { // Проверяем, если это наш NetworkError (уже выброшенный из-за !response.ok) if (err instanceof NetworkError) { throw err; // Перебрасываем нашу специфичную ошибку } // Если это была другая ошибка (например, из Promise.reject до создания Response) if (err instanceof Response) { // Если reject вернул сам Response объект throw new NetworkError(`Неожиданный сетевой ответ: ${err.statusText}`, err.status); } // В противном случае, оборачиваем в стандартную ошибку или перебрасываем как есть throw new Error(`Неизвестная ошибка при запросе к ${url}: ${err}`); }}
// Обработка на верхнем уровнеasync function processUserAction() { try { // validateUserData("yasha", "invalid-email"); // Вызовет ValidationError // await fetchData("http://api.example.com/data"); // Успех // await fetchData("http://api.example.com/unauthorized"); // Вызовет NetworkError } catch (err: unknown) { if (err instanceof ValidationError) { console.error(`🚫 Ошибка валидации поля "${err.field}": ${err.message}`); } else if (err instanceof NetworkError) { console.error(`🌐 Ошибка сети (${err.statusCode}): ${err.message}`); } else if (err instanceof Error) { console.error(`❓ Неожиданная ошибка: ${err.message}`); } else { console.error("Критическая неизвестная ошибка:", err); } }}
processUserAction();🛠️ Продвинутые Приемы: Единообразие и Расширяемость
Заголовок раздела «🛠️ Продвинутые Приемы: Единообразие и Расширяемость»Иногда использование throw не всегда лучший подход, особенно когда ошибка является ожидаемым результатом операции (например, не найдена запись). В таких случаях, возвращение Result типа может быть более явным и предсказуемым.
Паттерн Result (Или Either)
Заголовок раздела «Паттерн Result (Или Either)»Идея проста: функция не throw’ит ошибку, а возвращает объект, который явно указывает, была ли операция успешной или нет, и содержит либо результат, либо ошибку. Это вдохновлено функциональным программированием.
// Тип, представляющий успешный результат или ошибкуtype Result<T, E> = { success: true; value: T } | { success: false; error: E };
class NotFoundError extends Error { constructor(message: string) { super(message); this.name = 'NotFoundError'; Object.setPrototypeOf(this, NotFoundError.prototype); }}
// Функция, которая "безопасно" ищет пользователяfunction findUserById(id: string): Result<{ id: string; name: string }, NotFoundError | Error> { if (id === '123') { return { success: true, value: { id: '123', name: 'Алиса' } }; } if (id === 'error-id') { return { success: false, error: new Error('Непредвиденная ошибка в базе данных.') }; } return { success: false, error: new NotFoundError(`Пользователь с ID ${id} не найден.`) };}
// Использование "безопасной" функцииconst userResult = findUserById('123');if (userResult.success) { console.log(`Найден пользователь: ${userResult.value.name}`);} else { // TypeScript теперь знает, что userResult.error может быть NotFoundError или Error if (userResult.error instanceof NotFoundError) { console.warn(`⚠️ Предупреждение: ${userResult.error.message}`); } else { console.error(`❌ Критическая ошибка: ${userResult.error.message}`); }}
const missingUserResult = findUserById('999');if (missingUserResult.success) { console.log(`Найден пользователь: ${missingUserResult.value.name}`);} else { console.error(`При поиске: ${missingUserResult.error.message}`);}Паттерн Result делает код более декларативным, так как все возможные исходы операции явно указаны в сигнатуре типа.
🐛 Типичные Ошибки и Как Их Избежать
Заголовок раздела «🐛 Типичные Ошибки и Как Их Избежать»-
“Заглатывание” ошибок (Swallowing Errors): Пустой
catchблок.try {riskyOperation(true);} catch {// 😱 Ничего не делаем! Ошибка просто исчезла.}Всегда либо логируйте ошибку, либо перебрасывайте (throw) ее, либо явно обрабатывайте.
-
Недостаточная детализация ошибок: Использование только
new Error()без контекста.- Решение: Создавайте свои классы ошибок, как мы показали выше, с дополнительными полями (коды, данные).
-
Неправильное обращение с
unknownвcatch: Попытка доступа к свойствам без проверки.try {throw { code: 500, message: "Сервер недоступен" }; // Выбрасываем объект} catch (error: unknown) {// console.error(error.message); // 💥 Ошибка компиляции: 'error' is of type 'unknown'.if (typeof error === 'object' && error !== null && 'message' in error) {console.error((error as { message: string }).message); // Только после сужения типа!}}- Решение: Всегда сужайте тип
unknownс помощьюinstanceof,typeof, или пользовательских Type Guards.
- Решение: Всегда сужайте тип
-
Игнорирование ошибок в промисах без
awaitили.catch():async function doSomethingAsync() {// Этот промис может завершиться ошибкой, но мы не await'им его и не добавляем .catch()// Ошибка будет необработанным исключением, если он завершится неудачно.Promise.reject("Ошибка в фоновом промисе!");}// doSomethingAsync(); // Вызовет Unhandled Promise Rejection- Решение: Всегда
awaitпромисы или добавляйте.catch()для их обработки.
- Решение: Всегда
🎯 Практика
Заголовок раздела «🎯 Практика»-
Пользовательский парсер JSON: Создайте класс
JsonParsingError, который наследуется отErrorи включает свойствоoriginalInput: string. Напишите функциюsafeJsonParse<T>(jsonString: string): Result<T, JsonParsingError | Error>, которая будет использоватьtry...catchдляJSON.parseи возвращатьResult. ЕслиJSON.parseвыбрасывает ошибку, функция должна обернуть ее вJsonParsingError. -
Функция авторизации с разными ошибками: Определите два новых класса ошибок:
UserNotFoundErrorиInvalidCredentialsError. Создайте асинхронную функциюauthenticate(username: string, passwordHash: string): Promise<Result<{ userId: string }, UserNotFoundError | InvalidCredentialsError | Error>>.- Имитируйте поиск пользователя: если
username=== “admin” иpasswordHash=== “hashed_password”, верните успех. - Если пользователь не найден, верните
UserNotFoundError. - Если пароль не совпадает, верните
InvalidCredentialsError. - Имитируйте неожиданную ошибку (например,
username=== “crash”) и верните стандартныйError.
- Имитируйте поиск пользователя: если
-
Декоратор для обработки асинхронных ошибок: (Продвинутое) Создайте декоратор метода
@catchAsyncErrors, который будет оборачивать асинхронный метод класса вtry...catch. Если метод выбрасывает ошибку, декоратор должен логировать ее и возвращатьPromise.reject(error)или, опционально,Promise.resolve(undefined)после обработки ошибки (например, отправки ее в аналитику).
💡 Совет
Заголовок раздела «💡 Совет»Используйте try...catch для непредвиденных исключений, которые могут нарушить выполнение программы (деление на ноль, проблемы с I/O, неожиданные ответы API). Для ожидаемых ошибок, которые являются частью бизнес-логики (неверный ввод, отсутствие записи, отказано в доступе), рассмотрите паттерн Result или явное возвращение ошибок, чтобы сделать потоки выполнения более прозрачными и типобезопасными. Всегда логируйте ошибки и предоставляйте пользователю осмысленную обратную связь, не раскрывая внутренних деталей реализации.