$ sudo teach IT
МОДУЛЬ 10 · УРОК 4

Обработка ошибок с типами

Типизированное управление ошибками в 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 вопросов

Result type pattern

Premium