Обработка ошибок с типами
Типизированное управление ошибками в TypeScript
📖 Введение
Представьте, что вы управляете рестораном. Если повар случайно добавил слишком много соли, вам нужно обработать эту проблему — либо добавить больше жидкости, либо начать блюдо заново. Если кассовый аппарат сломался, вы переключаетесь на ручной приём оплаты. В каждом случае у вас есть план действий для разных типов ошибок.
В программировании ошибки неизбежны. Файл может не загрузиться, сервер может не ответить, пользователь может ввести неверные данные. В JavaScript ошибки часто обрабатываются поверху — через общий try/catch, который ловит всё подряд и не знает, что именно произошло.
TypeScript даёт нам мощнейший инструмент — типизацию ошибок. Мы можем описать, какие именно ошибки может выдать функция, и TypeScript заставит нас обработать каждую из них. Это как иметь меню с описанием каждого блюда и рекомендацией — что делать, если оно оказалось слишком острым, слишком солёным или вовсе не тем, что вы заказывали.
В этом уроке мы детально разберём все аспекты типизированной обработки ошибок: от базовых типов ошибок до продвинутых паттернов как Result<T, E> и Either.
1 Встроенные типы ошибок в TypeScript
TypeScript и JavaScript предоставляют несколько встроенных классов ошибок. Каждый из них имеет свою специфику и используется в определённых ситуациях. Давайте рассмотрим основные из них:
// Error — базовый класс для всех ошибок
const err = new Error('Что-то пошло не по плану');
console.log(err.message); // 'Что-то пошло не по плану'
console.log(err.name); // 'Error'
console.log(err.stack); // Строка стека вызовов
// TypeError — ошибка типа данных
const num: number = undefined as any;
const arr: number[] = num as any;
arr.forEach(item => console.log(item));
// TypeError: num.forEach is not a function
// RangeError — значение за пределами допустимого диапазона
const arr2 = new Array(-1); // RangeError: Invalid array length
// SyntaxError — синтаксическая ошибка
eval('const x === 5'); // SyntaxError: Unexpected token '='
// ReferenceError — обращение к несуществующей переменной
// console.log(nonExistent); // ReferenceError: nonExistent is not defined
Однако в реальных проектах нам гораздо чаще приходится работать с кастомными (пользовательскими) ошибками, потому что встроенных типов не хватает для описаниябизнес-логики. Например, ошибка «пользователь не авторизован», «недостаточно средств на счёте» или «формат файла не поддерживается» — это не TypeError и не SyntaxError.
ℹ️ Информация: Все встроенные классы ошибок наследуются от Error. Это значит, что если вы ловите ошибку через catch, то она гарантированно будет экземпляром Error или его потомка.
2 Кастомные классы ошибок с типизацией
Создание пользовательских классов ошибок — первый и самый простой шаг к типизированной обработке ошибок. Мы наследуемся от Error и добавляем собственные свойства:
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.value = value;
}
}
class NotFoundError extends Error {
constructor(
message: string,
public readonly resource: string,
public readonly id: string | number
) {
super(message);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
}
}
class AuthenticationError extends Error {
constructor(
message: string,
public readonly statusCode: number = 401,
public readonly reason: 'expired' | 'invalid' | 'missing'
) {
super(message);
this.name = 'AuthenticationError';
this.statusCode = statusCode;
this.reason = reason;
}
}
class NetworkError extends Error {
constructor(
message: string,
public readonly url: string,
public readonly timeout: boolean = false,
public readonly retriesLeft: number = 0
) {
super(message);
this.name = 'NetworkError';
this.url = url;
this.timeout = timeout;
this.retriesLeft = retriesLeft;
}
}
Обратите внимание на несколько важных моментов:
💡 Совет: Используйте public readonly для свойств ошибок — ошибки не должны изменяться после создания. Это делает код предсказуемым и безопасным.
Теперь можно использовать эти ошибки и TypeScript будет знать об их структуре:
function validateEmail(email: string): void {
if (!email.includes('@')) {
throw new ValidationError(
'Некорректный формат email',
'email',
email
);
}
}
function findUser(id: number): User {
const user = db.users.findById(id);
if (!user) {
throw new NotFoundError(
`Пользователь с ID ${id} не найден`,
'User',
id
);
}
return user;
}
function login(username: string, password: string): User {
const session = authenticate(username, password);
if (!session) {
throw new AuthenticationError(
'Неверные учётные данные',
401,
'invalid'
);
}
return session.user;
}
async function fetchData(url: string): Promise<Data> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(
`HTTP ${response.status}: ${response.statusText}`,
url
);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
throw new NetworkError('Невозможно подключиться к серверу', url, true);
}
throw error;
}
}
3 unknown vs any для ошибок
Один из самых важных аспектов типизированной обработки ошибок — это выбор типа для переменной, в которую попадает пойманная ошибка. TypeScript предоставляет два варианта: any и unknown. Разница между ними колоссальна.
// ❌ ОПАСНО: any — можно делать что угодно
try {
riskyOperation();
} catch (error: any) {
console.log(error.message); // OK
console.log(error.code); // OK — TypeScript не проверит!
console.log(error.foo.bar.baz); // OK — упадёт в рантайме!
error.someMethod(); // OK — упадёт в рантайме!
}
// ✅ БЕЗОПАСНО: unknown — нужно сначала проверить тип
try {
riskyOperation();
} catch (error: unknown) {
// console.log(error.message); // ОШИБКА компиляции!
// TypeScript заставит вас сначала проверить тип
if (error instanceof Error) {
console.log(error.message); // OK — тип narrowed
}
if (error instanceof ValidationError) {
console.log(error.field); // OK — TypeScript знает о field
console.log(error.value); // OK — TypeScript знает о value
}
}
Почему any так опасен для ошибок? Потому что any отключает проверку типов. Это значит, что TypeScript не поможет вам, если ошибка неожиданного типа. Вы можете обратиться к свойствам, которых нет, и компилятор не предупредит.
unknown — это безопасная альтернатива. Он говорит: «Я не знаю, что это за тип, но я буду знать, когда выяснится». TypeScript не позволяет обращаться к свойствам unknown без предварительной проверки типа.
⚠️ Внимание: В TypeScript 4.4+ по умолчанию переменная error в catch-блоке имеет тип unknown (если включена опция useUnknownInCatchVariables). Это правильный подход по умолчанию!
4 Type narrowing для ошибок
Type narrowing (сужение типа) — это ключевой механизм TypeScript, который позволяет превратить unknown в конкретный тип. Для ошибок доступны несколько способов narrowing:
// Способ 1: instanceof — проверка принадлежности к классу
function handleError(error: unknown): string {
if (error instanceof ValidationError) {
// error: ValidationError —знает field и value
return `Ошибка в поле "${error.field}": ${error.message}`;
}
if (error instanceof NotFoundError) {
// error: NotFoundError —знает resource и id
return `${error.resource} с ID ${error.id} не найден`;
}
if (error instanceof AuthenticationError) {
// error: AuthenticationError —знает statusCode и reason
if (error.reason === 'expired') {
return 'Сессия истекла, пожалуйста, войдите заново';
}
return `Ошибка аутентификации (${error.statusCode})`;
}
if (error instanceof NetworkError) {
// error: NetworkError —знает url, timeout, retriesLeft
if (error.timeout && error.retriesLeft > 0) {
return `Таймаут при подключении к ${error.url}. Повтор...`;
}
return `Сетевая ошибка: ${error.message}`;
}
if (error instanceof Error) {
// error: Error — стандартная ошибка
return `Неожиданная ошибка: ${error.message}`;
}
// error: unknown — не Error!
return `Неизвестная ошибка: ${String(error)}`;
}
// Способ 2: Проверка свойств (duck typing)
function handleErrorDuck(error: unknown): string {
if (
typeof error === 'object' &&
error !== null &&
'field' in error &&
'value' in error
) {
const ve = error as { field: string; value: unknown; message: string };
return `Ошибка в поле "${ve.field}"`;
}
// ...
return 'Неизвестная ошибка';
}
// Способ 3: Discriminated union с типом 'type'
type AppError =
| { type: 'validation'; field: string; message: string }
| { type: 'network'; url: string; timeout: boolean }
| { type: 'auth'; reason: 'expired' | 'invalid' }
| { type: 'unknown'; message: string };
function handleErrorDiscriminated(error: AppError): string {
switch (error.type) {
case 'validation':
return `Ошибка валидации поля "${error.field}": ${error.message}`;
case 'network':
return error.timeout
? `Таймаут при подключении к ${error.url}`
: `Сетевая ошибка: ${error.url}`;
case 'auth':
return error.reason === 'expired'
? 'Сессия истекла'
: 'Неверные учётные данные';
case 'unknown':
return error.message;
}
}
Каждый способ имеет свои преимущества:
instanceof
Работает с классами, проверяет прототипную цепочку
'in' operator
Работает с объектами без классов, проверяет наличие свойства
Discriminated unions
Самый мощный способ — полная проверка через switch
Custom type guard
Переиспользуемые функции проверки типа
5 Type guards для ошибок
Type guard — это функция, которая проверяет тип во время выполнения и возвращает true, если значение принадлежит указанному типу. Для ошибок это особенно полезно, потому что мы можем переиспользовать логику проверки:
// Type guard с использованием 'function' type predicate
function isValidationError(error: unknown): error is ValidationError {
return (
error instanceof Error &&
error.name === 'ValidationError' &&
'field' in error &&
'value' in error
);
}
function isNetworkError(error: unknown): error is NetworkError {
return (
error instanceof Error &&
error.name === 'NetworkError' &&
'url' in error
);
}
function isAuthenticationError(error: unknown): error is AuthenticationError {
return (
error instanceof Error &&
error.name === 'AuthenticationError' &&
'statusCode' in error &&
'reason' in error
);
}
// Теперь можно использовать эти guard-функции
function processRequest(data: unknown): string {
try {
const result = validateAndProcess(data);
return result;
} catch (error: unknown) {
if (isValidationError(error)) {
// error: ValidationError — TypeScript знает тип
reportFieldError(error.field, error.message);
return `Поле "${error.field}" содержит ошибку`;
}
if (isNetworkError(error)) {
// error: NetworkError
if (error.retriesLeft > 0) {
scheduleRetry(error.url);
return `Повторная попытка для ${error.url}`;
}
return `Не удалось подключиться к ${error.url}`;
}
if (isAuthenticationError(error)) {
// error: AuthenticationError
redirectToLogin();
return 'Необходима авторизация';
}
throw error; // Пробрасываем неизвестные ошибки дальше
}
}
// Type guard для не Error объектов
function isHttpError(
error: unknown
): error is { status: number; statusText: string; body: unknown } {
return (
typeof error === 'object' &&
error !== null &&
'status' in error &&
typeof (error as any).status === 'number'
);
}
// Type guard для строковых ошибок
function isStringError(error: unknown): error is string {
return typeof error === 'string';
}
💡 Совет: Создавайте type guard-функции в отдельном файле (например, guards.ts) и импортируйте их по мере необходимости. Это делает код чище и переиспользуемее.
6 Паттерн Result<T, E>
Result<T, E> — это один из самых мощных паттернов обработки ошибок в TypeScript. Идея проста: функция не выбрасывает ошибку, а возвращает объект, который либо содержит успешный результат типа T, либо ошибку типа E.
Это как в ресторане: вам не бросают тарелку в лицо, если блюдо не удалось. Вместо этого официант возвращает блюдо и говорит: «К сожалению, шеф-повар не смог приготовить это блюдо, но вот альтернатива».
// Определение типа Result
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
// Фабричные функции
function Ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function Err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Пример использования
type ParseError = { type: 'parse'; message: string; input: string };
type ValidationError = { type: 'validation'; field: string; message: string };
type AppError = ParseError | ValidationError;
function parseAge(input: string): Result<number, AppError> {
const trimmed = input.trim();
if (!/^\d+$/.test(trimmed)) {
return Err({
type: 'parse',
message: 'Возраст должен быть числом',
input: trimmed,
});
}
const age = parseInt(trimmed, 10);
if (age < 0 || age > 150) {
return Err({
type: 'validation',
field: 'age',
message: 'Возраст должен быть от 0 до 150',
});
}
return Ok(age);
}
// Обработка результата
function processAge(input: string): string {
const result = parseAge(input);
if (result.ok) {
// result.value: number
return `Возраст пользователя: ${result.value}`;
}
// result.error: AppError
switch (result.error.type) {
case 'parse':
return `Ошибка парсинга: ${result.error.message} (вход: "${result.error.input}")`;
case 'validation':
return `Ошибка валидации поля "${result.error.field}": ${result.error.message}`;
}
}
console.log(processAge('25')); // 'Возраст пользователя: 25'
console.log(processAge('abc')); // 'Ошибка парсинга: Возраст должен быть числом (вход: "abc")'
console.log(processAge('200')); // 'Ошибка валидации поля "age": Возраст должен быть от 0 до 150'
Продвинутая версия Result с вспомогательными методами:
class ResultImpl<T, E> {
private constructor(
private readonly _ok: boolean,
private readonly _value?: T,
private readonly _error?: E
) {}
static ok<T>(value: T): ResultImpl<T, never> {
return new ResultImpl(true, value);
}
static err<E>(error: E): ResultImpl<never, E> {
return new ResultImpl(false, undefined, error);
}
get isOk(): boolean {
return this._ok;
}
get isErr(): boolean {
return !this._ok;
}
get value(): T {
if (!this._ok) {
throw new Error('Attempted to access value on Err');
}
return this._value!;
}
get error(): E {
if (this._ok) {
throw new Error('Attempted to access error on Ok');
}
return this._error!;
}
map<U>(fn: (value: T) => U): ResultImpl<U, E> {
return this._ok
? ResultImpl.ok(fn(this._value!))
: ResultImpl.err(this._error!);
}
flatMap<U>(fn: (value: T) => ResultImpl<U, E>): ResultImpl<U, E> {
return this._ok ? fn(this._value!) : ResultImpl.err(this._error!);
}
mapError<F>(fn: (error: E) => F): ResultImpl<T, F> {
return this._ok
? ResultImpl.ok(this._value!)
: ResultImpl.err(fn(this._error!));
}
unwrapOr(defaultValue: T): T {
return this._ok ? this._value! : defaultValue;
}
unwrapOrElse(fn: (error: E) => T): T {
return this._ok ? this._value! : fn(this._error!);
}
match<U>(onOk: (value: T) => U, onErr: (error: E) => U): U {
return this._ok ? onOk(this._value!) : onErr(this._error!);
}
}
// Пример использования продвинутого Result
const result = parseAge('25')
.map(age => age * 2) // ResultImpl<number, AppError>
.map(age => `Возраст × 2 = ${age}`); // ResultImpl<string, AppError>
console.log(result.value); // 'Возраст × 2 = 50'
const fallback = parseAge('abc')
.unwrapOr(0);
console.log(fallback); // 0
7 Either моноид (монада)
Either<L, R> — это паттерн из функционального программирования, который является обобщением Result<T, E>. L (Left) — это «левая» сторона (обычно ошибка), а R (Right) — это «правая» сторона (успех).
Конвенция: Left содержит ошибку, Right содержит успех. Это как левая рука — та, которая неудобная (с ошибкой), и правая — удобная (с результатом).
// Базовая реализация Either
type Either<L, R> = Left<L> | Right<R>;
interface Left<L> {
readonly _tag: 'Left';
readonly left: L;
}
interface Right<R> {
readonly _tag: 'Right';
readonly right: R;
}
function left<L>(l: L): Either<L, never> {
return { _tag: 'Left', left: l };
}
function right<R>(r: R): Either<never, R> {
return { _tag: 'Right', right: r };
}
// Проверка типа
function isLeft<L, R>(e: Either<L, R>): e is Left<L> {
return e._tag === 'Left';
}
function isRight<L, R>(e: Either<L, R>): e is Right<R> {
return e._tag === 'Right';
}
// Паттерн-матчинг (fold)
function fold<L, R, T>(
onLeft: (l: L) => T,
onRight: (r: R) => T,
e: Either<L, R>
): T {
return isLeft(e) ? onLeft(e.left) : onRight(e.right);
}
// Пример использования
type ApiError = { code: number; message: string };
type UserData = { id: number; name: string; email: string };
function fetchUser(id: number): Either<ApiError, UserData> {
try {
const user = database.findUser(id);
if (!user) {
return left({ code: 404, message: 'Пользователь не найден' });
}
return right({ id: user.id, name: user.name, email: user.email });
} catch {
return left({ code: 500, message: 'Внутренняя ошибка сервера' });
}
}
// Цепочка операций
function getUserDisplayName(id: number): string {
return fold(
(err) => `Ошибка: ${err.message} (код ${err.code})`,
(user) => `Пользователь: ${user.name} (${user.email})`,
fetchUser(id)
);
}
// Маппинг
function mapEither<L, R, R2>(
fn: (r: R) => R2,
e: Either<L, R>
): Either<L, R2> {
return isRight(e) ? right(fn(e.right)) : e;
}
const result = mapEither(
(user) => user.name.toUpperCase(),
fetchUser(1)
);
// Either<ApiError, string>
8 Типизация catch-блоков
В JavaScript переменная error в catch может быть чем угодно — строкой, объектом, null, undefined. TypeScript 4.4+ вводит опцию useUnknownInCatchVariables, которая по умолчанию типизирует ошибку как unknown.
// tsconfig.json
{
"compilerOptions": {
"useUnknownInCatchVariables": true // по умолчанию true в 4.4+
}
}
// Без useUnknownInCatchVariables (старое поведение)
try {
riskyOperation();
} catch (error: any) {
// error: any — TypeScript не проверяет обращения
console.log(error.message); // OK для компилятора, может упасть в рантайме
}
// С useUnknownInCatchVariables (новое поведение)
try {
riskyOperation();
} catch (error: unknown) {
// error: unknown — TypeScript заставит проверить тип
// Безопасное извлечение сообщения об ошибке
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (
typeof error === 'object' &&
error !== null &&
'message' in error
) {
return String((error as { message: unknown }).message);
}
return String(error);
}
const message = getErrorMessage(error);
console.error(message);
}
// Утилита для безопасного извлечения ошибки
function safeGetError(error: unknown): {
message: string;
stack?: string;
name: string;
} {
if (error instanceof Error) {
return {
message: error.message,
stack: error.stack,
name: error.name,
};
}
if (typeof error === 'string') {
return { message: error, name: 'StringError' };
}
return { message: String(error), name: 'UnknownError' };
}
// Утилита для проверки, является ли значение ошибкой
function isErrorLike(value: unknown): value is Error {
return (
value instanceof Error ||
(typeof value === 'object' &&
value !== null &&
'message' in value &&
typeof (value as any).message === 'string')
);
}
9 Типизация ошибок в async/await
Асинхронные функции в TypeScript возвращают Promise<T>. Но тип ошибки не отражается в сигнатуре функции — Promise<T> не знает, какие ошибки могут быть выброшены. Это проблема, и существует несколько способов её решения:
// Проблема: TypeScript не знает, какие ошибки могут быть
async function fetchUserProfile(userId: number): Promise<UserProfile> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new NetworkError(`HTTP ${response.status}`, `/api/users/${userId}`);
}
const data = await response.json();
if (!data.name) {
throw new ValidationError('Отсутствует поле name', 'name', data);
}
return data;
}
// Вызывающий код не знает, какие ошибки могут быть!
try {
const profile = await fetchUserProfile(1);
console.log(profile.name);
} catch (error: unknown) {
// Какие типы ошибок здесь возможны? TypeScript не поможет.
}
// Решение 1: Использовать Result для async функций
async function fetchUserProfileSafe(
userId: number
): Promise<Result<UserProfile, ApiError>> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
return Err({ type: 'network', status: response.status, url: `/api/users/${userId}` });
}
const data = await response.json();
if (!data.name) {
return Err({ type: 'validation', field: 'name', message: 'Отсутствует поле name' });
}
return Ok(data);
} catch (error) {
return Err({
type: 'unknown',
message: error instanceof Error ? error.message : String(error),
});
}
}
// Решение 2: Custom wrapper для Promise с ошибками
type AsyncResult<T, E> = Promise<Result<T, E>>
async function processOrder(orderId: number): AsyncResult<Order, OrderError> {
const orderResult = await getOrder(orderId);
if (!orderResult.ok) return orderResult;
const validateResult = validateOrder(orderResult.value);
if (!validateResult.ok) return validateResult;
return Ok(await submitOrder(validateResult.value));
}
10 try/catch с типизированными ошибками: полные примеры
Давайте рассмотрим реальный пример — обработку заказа в интернет-магазине, где множество вещей может пойти не так:
// Типы ошибок для нашего приложения
type CheckoutError =
| { kind: 'cart_empty'; message: string }
| { kind: 'product_not_found'; productId: number; name: string }
| { kind: 'insufficient_stock'; productId: number; available: number; requested: number }
| { kind: 'payment_failed'; reason: string; provider: string }
| { kind: 'address_invalid'; field: string; value: string }
| { kind: 'network'; url: string; timeout: boolean }
| { kind: 'unknown'; originalError: unknown };
// Функция оформления заказа
async function checkout(
cart: Cart,
address: Address,
paymentMethod: PaymentMethod
): Promise<Result<OrderConfirmation, CheckoutError>> {
// Проверка корзины
if (cart.items.length === 0) {
return Err({
kind: 'cart_empty',
message: 'Корзина пуста. Добавьте товары перед оформлением заказа.',
});
}
// Проверка наличия товаров
for (const item of cart.items) {
const product = await getProduct(item.productId);
if (!product) {
return Err({
kind: 'product_not_found',
productId: item.productId,
name: item.name,
});
}
if (product.stock < item.quantity) {
return Err({
kind: 'insufficient_stock',
productId: item.productId,
available: product.stock,
requested: item.quantity,
});
}
}
// Проверка адреса
if (!address.street || !address.city || !address.zip) {
return Err({
kind: 'address_invalid',
field: !address.street ? 'street' : !address.city ? 'city' : 'zip',
value: !address.street ? address.street : !address.city ? address.city : address.zip,
});
}
// Попытка оплаты
try {
const payment = await processPayment(cart.total, paymentMethod);
if (!payment.success) {
return Err({
kind: 'payment_failed',
reason: payment.error || 'Неизвестная ошибка платёжной системы',
provider: paymentMethod.provider,
});
}
} catch (error: unknown) {
if (error instanceof TypeError && error.message.includes('timeout')) {
return Err({
kind: 'network',
url: '/api/payment',
timeout: true,
});
}
return Err({
kind: 'unknown',
originalError: error,
});
}
// Создание заказа
const order = await createOrder(cart, address, paymentMethod);
return Ok(order);
}
// Обработка результата на стороне клиента
async function handleCheckout(cart: Cart, address: Address, payment: PaymentMethod) {
const result = await checkout(cart, address, payment);
if (result.ok) {
showSuccess(`Заказ #${result.value.id} оформлен!`);
return;
}
const err = result.error;
switch (err.kind) {
case 'cart_empty':
showMessage('Ваша корзина пуста');
break;
case 'product_not_found':
showMessage(`Товар "${err.name}" больше не доступен`);
removeFromCart(err.productId);
break;
case 'insufficient_stock':
showMessage(`Доступно только ${err.available} шт.`);
updateCartItemQuantity(err.productId, err.available);
break;
case 'payment_failed':
showMessage(`Ошибка оплаты: ${err.reason}. Попробуйте другой способ.`);
break;
case 'address_invalid':
highlightField(err.field);
showMessage(`Проверьте поле "${err.field}"`);
break;
case 'network':
showMessage(err.timeout ? 'Сервер не отвечает. Попробуйте позже.' : 'Ошибка сети');
break;
case 'unknown':
reportError(err.originalError);
showMessage('Произошла неожиданная ошибка. Попробуйте позже.');
break;
}
}
11 Типизация Promise.catch
Метод .catch() у Promise принимает callback, в котором параметр ошибки имеет тип any (или unknown с новыми настройками). Это значит, что внутри .catch() вамтакженужно narrowing:
// Типичное использование .catch() с обработкой ошибок
fetchUser(userId)
.then((user) => processUser(user))
.catch((error: unknown) => {
// error: unknown — нужно проверять тип
if (error instanceof ValidationError) {
console.error(`Ошибка валидации: ${error.field}`);
} else if (error instanceof NetworkError) {
console.error(`Сетевая ошибка: ${error.url}`);
} else if (error instanceof Error) {
console.error(`Общая ошибка: ${error.message}`);
} else {
console.error(`Неизвестная ошибка: ${String(error)}`);
}
});
// Утилита для создания типизированного .catch()
function typedCatch<T, E extends Error>(
promise: Promise<T>,
errorClass: new (...args: any[]) => E,
handler: (error: E) => T | Promise<T>
): Promise<T> {
return promise.catch((error: unknown) => {
if (error instanceof errorClass) {
return handler(error);
}
throw error; // Пробрасываем неизвестные ошибки
});
}
// Использование
typedCatch(
fetch('/api/data'),
NetworkError,
(error) => {
console.log(`Повтор для ${error.url}...`);
return fetch(error.url); // Retry
}
);
// Более продвинутая версия с несколькими обработчиками
interface CatchHandler<T> {
errorClass: new (...args: any[]) => Error;
handler: (error: any) => T | Promise<T>;
}
function multiCatch<T>(
promise: Promise<T>,
...handlers: CatchHandler<T>[]
): Promise<T> {
return promise.catch((error: unknown) => {
for (const { errorClass, handler } of handlers) {
if (error instanceof errorClass) {
return handler(error);
}
}
throw error;
});
}
// Использование
multiCatch(
fetchUser(userId),
{
errorClass: ValidationError,
handler: (e) => ({ id: 0, name: 'Гость', email: '' }),
},
{
errorClass: NetworkError,
handler: (e) => fetchFromCache(userId),
}
);
12 Discriminated unions для ошибок
Discriminated unions (дискриминированные объединения) — это самый мощный способ представления ошибок в TypeScript. Каждый вариант ошибки имеет уникальное свойство-дискриминатор, по которому TypeScript может однозначно определить тип:
// Дискриминированное объединение ошибок
type ApiError =
| {
kind: 'not_found';
resource: string;
id: string | number;
}
| {
kind: 'validation';
field: string;
constraint: string;
received: unknown;
}
| {
kind: 'unauthorized';
reason: 'expired' | 'invalid' | 'missing';
}
| {
kind: 'rate_limited';
retryAfter: number;
}
| {
kind: 'server';
statusCode: number;
traceId: string;
}
| {
kind: 'network';
url: string;
timeout: boolean;
};
// Exhaustive check — проверка всех вариантов
function handleApiError(error: ApiError): string {
switch (error.kind) {
case 'not_found':
return `${error.resource}#${error.id} не найден`;
case 'validation':
return `Поле "${error.field}" не удовлетворяет ограничению "${error.constraint}"`;
case 'unauthorized':
switch (error.reason) {
case 'expired':
return 'Токен истёк. Обновите сессию.';
case 'invalid':
return 'Неверный токен авторизации.';
case 'missing':
return 'Токен авторизации отсутствует.';
}
case 'rate_limited':
return `Слишком много запросов. Повторите через ${error.retryAfter} сек.`;
case 'server':
return `Ошибка сервера ${error.statusCode}. Trace ID: ${error.traceId}`;
case 'network':
return error.timeout
? `Таймаут подключения к ${error.url}`
: `Ошибка сети при подключении к ${error.url}`;
default:
// TypeScript убедится, что мы обработали все варианты!
const _exhaustive: never = error;
return _exhaustive;
}
}
// Функция, возвращающая Result с дискриминированными ошибками
async function apiCall<T>(
url: string,
options?: RequestInit
): Promise<Result<T, ApiError>> {
try {
const response = await fetch(url, options);
if (response.status === 404) {
return Err({ kind: 'not_found', resource: url, id: url });
}
if (response.status === 401) {
const body = await response.json();
return Err({
kind: 'unauthorized',
reason: body.reason || 'invalid',
});
}
if (response.status === 422) {
const body = await response.json();
return Err({
kind: 'validation',
field: body.field,
constraint: body.constraint,
received: body.value,
});
}
if (response.status === 429) {
return Err({
kind: 'rate_limited',
retryAfter: parseInt(response.headers.get('Retry-After') || '60'),
});
}
if (response.status >= 500) {
return Err({
kind: 'server',
statusCode: response.status,
traceId: response.headers.get('X-Trace-Id') || 'unknown',
});
}
if (!response.ok) {
return Err({
kind: 'server',
statusCode: response.status,
traceId: response.headers.get('X-Trace-Id') || 'unknown',
});
}
const data: T = await response.json();
return Ok(data);
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') {
return Err({ kind: 'network', url, timeout: true });
}
return Err({ kind: 'network', url, timeout: false });
}
}
ℹ️ Информация: Свойство kind (или type, code) — это дискриминатор. TypeScript использует его в switch для определения точного типа ошибки.
13 Тип never в обработке ошибок
Тип never в обработке ошибок используется как «ловушка» для exhaustive checking (исчерпывающей проверки). Если вы забыли обработать какой-то вариант ошибки, TypeScript выдаст ошибку компиляции:
// Exhaustive checking с never
type ErrorCode = 'AUTH_ERROR' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR';
function getErrorTitle(code: ErrorCode): string {
switch (code) {
case 'AUTH_ERROR':
return 'Ошибка авторизации';
case 'NOT_FOUND':
return 'Не найдено';
case 'SERVER_ERROR':
return 'Ошибка сервера';
case 'NETWORK_ERROR':
return 'Ошибка сети';
default:
const _exhaustive: never = code;
return _exhaustive;
}
}
// Если мы добавим новый вариант в ErrorCode:
type ErrorCode2 = 'AUTH_ERROR' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR';
function getErrorTitle2(code: ErrorCode2): string {
switch (code) {
case 'AUTH_ERROR':
return 'Ошибка авторизации';
case 'NOT_FOUND':
return 'Не найдено';
case 'SERVER_ERROR':
return 'Ошибка сервера';
case 'NETWORK_ERROR':
return 'Ошибка сети';
// Забыли обработать TIMEOUT_ERROR!
default:
// TypeScript выдаст ошибку:
// Type 'TIMEOUT_ERROR' is not assignable to type 'never'
const _exhaustive: never = code;
return _exhaustive;
}
}
// never для Result
function neverReached(x: never): never {
throw new Error(`Неожиданное значение: ${x}`);
}
function processResult<T, E>(result: Result<T, E>): string {
if (result.ok) {
return `Успех: ${result.value}`;
}
// Здесь result.error: E
// Если E — это дискриминированное union, мы можем сделать exhaustive check
const error = result.error;
// Пример с конкретным типом
if (typeof error === 'object' && error !== null && 'kind' in error) {
switch (error.kind) {
case 'not_found':
return 'Ресурс не найден';
case 'validation':
return 'Ошибка валидации';
default:
return neverReached(error as never);
}
}
return String(error);
}
never в Result<T, never> означает, что функция гарантированно не вернёт ошибку:
// Result с never означает: только успех
function parseNumber(input: string): Result<number, never> {
const num = Number(input);
if (isNaN(num)) {
// Здесь нам нужно вернуть ошибку, но тип never говорит:
// "этого не должно произойти по типу"
throw new Error('Эта ошибка не типизирована');
}
return Ok(num);
}
// Result с never в error позиции
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}
// Пример: функция, которая всегда успешна
function getConstant(): Result<42, never> {
return Ok(42);
}
14 Unhandled promise rejection и обработка
Unhandled promise rejection происходит, когда Promise отклоняется, а обработчик .catch() или try/catch не поймал ошибку. Это одна из самых опасных проблем в JavaScript — приложение может молча упасть.
// ❌ ОПАСНО: unhandled rejection
async function dangerous() {
const data = await fetch('/api/data'); // Может отклониться!
return data.json(); // Может отклониться!
// Если оба отклонения не обработаны — unhandled rejection
}
// ✅ Безопасно: обработка на каждом шаге
async function safe(): Promise<Result<Data, Error>> {
try {
const response = await fetch('/api/data');
if (!response.ok) {
return Err(new Error(`HTTP ${response.status}`));
}
const data = await response.json();
return Ok(data);
} catch (error) {
return Err(
error instanceof Error ? error : new Error(String(error))
);
}
}
// Обработка unhandled rejection на уровне приложения (Node.js)
if (typeof process !== 'undefined') {
process.on('unhandledRejection', (reason: unknown, promise) => {
console.error('Unhandled Rejection at:', promise);
console.error('Reason:', reason);
// Отправка в систему мониторинга
Sentry.captureException(reason);
});
}
// В браузере
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // Предотвращаем вывод в консоль по умолчанию
});
}
// Утилита для безопасного вызова async функций
async function safeAsync<T>(
fn: () => Promise<T>
): Promise<Result<T, Error>> {
try {
const value = await fn();
return Ok(value);
} catch (error) {
return Err(
error instanceof Error ? error : new Error(String(error))
);
}
}
// Использование
const result = await safeAsync(() => fetchUser(1));
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error.message);
}
⚠️ Внимание: В Node.js 15+ unhandled rejection приводит к завершению процесса. Всегда обрабатывайте ошибки в async функциях!
15 Error Boundaries в React с типизацией
Error Boundary — это компонент React, который ловит ошибки в дочерних компонентах и отображает запасной UI вместо падения всего дерева. В TypeScript мы можем типизировать ошибки, которые ловит Error Boundary:
import React, { Component, ErrorInfo, ReactNode } from 'react';
// Типизация ошибок Error Boundary
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo);
// Отправка в систему мониторинга
console.error('ErrorBoundary caught:', error, errorInfo);
}
reset = (): void => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render(): ReactNode {
if (this.state.hasError && this.state.error) {
if (typeof this.props.fallback === 'function') {
return this.props.fallback(this.state.error, this.reset);
}
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div style={{ padding: 20, border: '1px solid #f00', borderRadius: 8 }}>
<h2>Произошла ошибка</h2>
<p>{this.state.error.message}</p>
<button onClick={this.reset}>Попробовать снова</button>
</div>
);
}
return this.props.children;
}
}
// Использование с типизированным fallback
function App() {
return (
<ErrorBoundary
onError={(error, info) => {
reportToAnalytics({ error: error.message, stack: info.componentStack });
}}
fallback={(error, reset) => (
<div>
<h1>Что-то пошло не так</h1>
<p>{error.message}</p>
<button onClick={reset}>Повторить</button>
</div>
)}
>
<Dashboard />
</ErrorBoundary>
);
}
// Хук для использования в функциональных компонентах
function useErrorHandler() {
return (error: Error) => {
// В React 18+ можно использовать onError
console.error(error);
};
}
16 Библиотека ts-results
ts-results — это полноценная библиотека для работы с паттерном Result в TypeScript. Она предоставляет готовые типы Ok, Err, Result и множество вспомогательных функций:
// npm install ts-results
import { Ok, Err, Result } from 'ts-results';
// Создание Result
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return Err('Деление на ноль');
}
return Ok(a / b);
}
// Цепочки операций
const result = divide(10, 2)
.map((x) => x * 2) // Result<number, string>
.map((x) => `Результат: ${x}`) // Result<string, string>
.mapErr((e) => `Ошибка: ${e}`); // Result<string, string>
console.log(result.unwrap()); // 'Результат: 10'
// Result.combine — объединение нескольких Result
const name = Ok('Иван');
const age = Ok(25);
const email = Ok('ivan@example.com');
const combined = Result.combine([name, age, email]);
// Result<[string, number, string], never>
// Async операции
async function fetchUserData(
id: number
): Promise<Result<User, ApiError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Err({ code: response.status, message: response.statusText });
}
return Ok(await response.json());
} catch (error) {
return Err({
code: 0,
message: error instanceof Error ? error.message : 'Network error',
});
}
}
// Удобная обработка
async function showUser(id: number) {
const result = await fetchUserData(id);
result
.map((user) => {
document.getElementById('name')!.textContent = user.name;
document.getElementById('email')!.textContent = user.email;
})
.mapErr((err) => {
document.getElementById('error')!.textContent =
`Ошибка ${err.code}: ${err.message}`;
});
}
// toAsyncResult — преобразование Promise<Result> в Result
async function processWithTimeout<T, E>(
operation: () => Promise<Result<T, E>>,
timeoutMs: number
): Promise<Result<T, E | 'timeout'>> {
return Promise.race([
operation(),
new Promise<Result<never, 'timeout'>>((resolve) =>
setTimeout(() => resolve(Err('timeout')), timeoutMs)
),
]);
}
17 Библиотека neverthrow
neverthrow — ещё одна популярная библиотека для Result. Она отличается от ts-results подходом к типизации ипредоставляетбольше функциональности:
// npm install neverthrow
import { ok, err, Result, okAsync, errAsync } from 'neverthrow';
// Создание Result
function parseJSON(input: string): Result<unknown, Error> {
try {
return ok(JSON.parse(input));
} catch (e) {
return err(new Error(`Невалидный JSON: ${e}`));
}
}
// Цепочки с .andThen (flatMap)
function parseAndValidate(
input: string
): Result<ValidData, ValidationError> {
return parseJSON(input)
.andThen((data) => {
if (typeof data !== 'object' || data === null) {
return err(new ValidationError('Данные должны быть объектом'));
}
return ok(data);
})
.andThen((data) => {
if (!('name' in data) || !('email' in data)) {
return err(new ValidationError('Отсутствуют обязательные поля'));
}
return ok(data as ValidData);
});
}
// Async Result
function createOrder(
items: CartItem[]
): ResultAsync<Order, OrderError> {
return validateItems(items)
.andThen((validatedItems) =>
calculateTotal(validatedItems)
)
.andThen((total) =>
processPayment(total)
)
.andThen((payment) =>
saveOrder(payment)
);
}
// match — паттерн-матчинг
function handleResult<T, E>(result: Result<T, E>) {
result.match(
(value) => console.log('Успех:', value),
(error) => console.error('Ошибка:', error)
);
}
// combine — объединение результатов
import { Result.combine } from 'neverthrow';
const results = [validateName('Иван'), validateAge(25), validateEmail('a@b.c')];
const combined = Result.combine(results);
// Result<[string, number, string], ValidationError[]>
18 Типизация ошибок в Express.js
Express.js не имеет встроенной типизации для ошибок. Middleware для обработки ошибок принимает 4 аргумента, и TypeScript не всегда понимает их типы. Вот как правильно типизировать ошибки в Express:
import { Request, Response, NextFunction } from 'express';
// Кастомная ошибка с HTTP-статусом
class AppError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly code: string,
public readonly isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
}
}
class NotFoundError extends AppError {
constructor(resource: string, id?: string | number) {
super(
`${resource}${id ? `#${id}` : ''} не найден`,
404,
'NOT_FOUND'
);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(
public readonly field: string,
message: string
) {
super(message, 422, 'VALIDATION_ERROR');
this.name = 'ValidationError';
}
}
// Типизированный middleware для обработки ошибок
function errorHandler(
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void {
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
...(err instanceof ValidationError && { field: err.field }),
},
});
return;
}
// Неизвестная ошибка
console.error('Unexpected error:', err);
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'Внутренняя ошибка сервера',
},
});
}
// Async handler wrapper — автоматическая обработка ошибок
function asyncHandler<T extends Request>(
fn: (req: T, res: Response, next: NextFunction) => Promise<void>
) {
return (req: T, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
// Использование
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
throw new ValidationError('id', 'ID должен быть числом');
}
const user = await db.users.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
res.json(user);
}));
19 Zod и типизированная валидация с ошибками
Zod — это библиотека валидации, которая интегрируется с TypeScript и возвращает типизированные ошибки. Она позволяет создавать схемы валидации и автоматически извлекать типы:
// npm install zod
import { z, ZodError } from 'zod';
// Определение схемы
const UserSchema = z.object({
name: z.string().min(2, 'Имя должно содержать минимум 2 символа'),
email: z.string().email('Некорректный формат email'),
age: z.number().int().positive().max(150),
role: z.enum(['admin', 'user', 'guest']),
});
// Извлечение TypeScript-типа из схемы
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number; role: 'admin' | 'user' | 'guest' }
// Валидация с типизированными ошибками
function validateUser(input: unknown): Result<User, ValidationError[]> {
const result = UserSchema.safeParse(input);
if (result.success) {
return Ok(result.data);
}
const errors: ValidationError[] = result.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
}));
return Err(errors);
}
// Использование
function createUser(input: unknown): Result<User, string> {
return validateUser(input)
.map((user) => {
// user: User — типизирован!
return {
...user,
createdAt: new Date(),
};
})
.mapErr((errors) => {
return errors.map((e) => `${e.field}: ${e.message}`).join('; ');
});
}
// Вложенные схемы
const OrderSchema = z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().positive(),
price: z.number().positive(),
})).min(1, 'Заказ должен содержать хотя бы один товар'),
shippingAddress: z.object({
street: z.string().min(1),
city: z.string().min(1),
zip: z.string().regex(/^\d{6}$/, 'Индекс должен содержать 6 цифр'),
}),
paymentMethod: z.enum(['card', 'cash', 'crypto']),
});
type Order = z.infer<typeof OrderSchema>;
20 Практический пример: типизированный API клиент
Давайте соберём всё вместе и создадим полноценный API клиент с типизированной обработкой ошибок:
// Определение всех возможных ошибок API
type ApiErrorKind =
| { type: 'network'; url: string; timeout: boolean }
| { type: 'http'; status: number; statusText: string; body?: unknown }
| { type: 'parse'; message: string; raw: string }
| { type: 'auth'; reason: 'expired' | 'invalid' | 'forbidden' }
| { type: 'validation'; errors: Array<{ field: string; message: string }> }
| { type: 'unknown'; cause: unknown };
// API клиент
class TypedApiClient {
private baseUrl: string;
private token?: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(path: string): Promise<Result<T, ApiErrorKind>> {
return this.request<T>(path, { method: 'GET' });
}
async post<T>(path: string, body: unknown): Promise<Result<T, ApiErrorKind>> {
return this.request<T>(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
private async request<T>(
path: string,
init: RequestInit
): Promise<Result<T, ApiErrorKind>> {
const url = `${this.baseUrl}${path}`;
const headers = new Headers(init.headers);
if (this.token) {
headers.set('Authorization', `Bearer ${this.token}`);
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
...init,
headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.status === 401) {
return Err({ type: 'auth', reason: 'expired' });
}
if (response.status === 403) {
return Err({ type: 'auth', reason: 'forbidden' });
}
if (response.status === 422) {
const body = await response.json();
return Err({
type: 'validation',
errors: body.errors || [],
});
}
if (!response.ok) {
return Err({
type: 'http',
status: response.status,
statusText: response.statusText,
});
}
const text = await response.text();
try {
const data = JSON.parse(text) as T;
return Ok(data);
} catch {
return Err({
type: 'parse',
message: 'Ошибка парсинга JSON',
raw: text,
});
}
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') {
return Err({ type: 'network', url, timeout: true });
}
if (error instanceof TypeError) {
return Err({ type: 'network', url, timeout: false });
}
return Err({ type: 'unknown', cause: error });
}
}
}
// Использование
const api = new TypedApiClient('https://api.example.com');
async function loadUserProfile(userId: number) {
const result = await api.get<UserProfile>(`/users/${userId}`);
result.match(
(profile) => {
document.getElementById('name')!.textContent = profile.name;
},
(error) => {
switch (error.type) {
case 'network':
if (error.timeout) {
showNotification('Сервер не отвечает. Попробуйте позже.');
} else {
showNotification('Ошибка сети. Проверьте подключение.');
}
break;
case 'auth':
redirectToLogin();
break;
case 'http':
showNotification(`Ошибка ${error.status}: ${error.statusText}`);
break;
case 'validation':
showValidationErrors(error.errors);
break;
default:
showNotification('Произошла неожиданная ошибка');
}
}
);
}
21 Рекомендации и лучшие практики
✅ Рекомендация 1: Используйте unknown вместо any для переменных ошибок. Это заставляет вас проверять тип перед использованием.
✅ Рекомендация 2: Используйте дискриминированные объединения для представления разных типов ошибок. Это обеспечивает полную типизацию и exhaustive checking.
✅ Рекомендация 3: Для критических бизнес-процессов используйте паттерн Result<T, E> вместо throw. Это делает потоки данных предсказуемыми.
✅ Рекомендация 4: Создавайте type guard-функции для проверки типов ошибок. Это делает код чище и переиспользуемее.
✅ Рекомендация 5: Для библиотек и публичных API используйте neverthrow или ts-results — онипредоставляет проверенные на практике паттерны.
✅ Рекомендация 6: Используйте never в default-ветке switch для exhaustive checking. Это гарантирует, что вы обработали все варианты ошибок.
❌ Антипаттерн: Не ловите все ошибки через один общий catch без проверки типа. Это скрывает реальные проблемы и затрудняет отладку.
❌ Антипаттерн: Не выбрасывайте строки вместо Error объектов. throw 'ошибка' не содержит стек вызовов и не наследуется от Error.
📝 Итоги урока
В этом уроке мы подробно изучили обработку ошибок с типами в TypeScript. Вот ключевые моменты:
1. unknown безопаснее any для ошибок — заставляет проверять тип
2. Дискриминированные объединения — лучший способ представления разных типов ошибок
3. Result<T, E> паттерн делает потоки данных предсказуемыми и исключает необработанные ошибки
4. Type guard-функции переиспользуют логику проверки и делают код чище
5. never в default-ветке switch обеспечивает exhaustive checking
6. Библиотеки ts-results и neverthrow предоставляют готовые решения для Result-паттерна
7. Всегда обрабатывайте ошибки в async функциях — unhandled rejection может убить приложение
Тест: Обработка ошибок с типами
10 вопросов