Типизация ошибок и обработка исключений в TypeScript

10 сентября 2026
Автор

Антон Ларичев

Почему типизация ошибок важна

В большинстве языков со статической типизацией ошибки строго типизированы. TypeScript исторически был исключением: в блоке catch переменная имела тип any, что лишало всех преимуществ типизации. С версии 4.0 ситуация изменилась — появилась опция useUnknownInCatchVariables, а в TypeScript 4.4 она вошла в строгий режим (strict: true).

Правильная типизация ошибок даёт три важных преимущества: компилятор не даст обратиться к несуществующим свойствам, IDE подсказывает доступные поля, а полное покрытие всех вариантов ошибок проверяется статически.

Курс по теме

Пройти курс TypeScript с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Тип unknown в catch-блоках

До TypeScript 4.0 переменная ошибки в catch всегда имела тип any:

try {
  // ...
} catch (error) {
  console.log(error.message); // TypeScript молчал, даже если message отсутствует
}

Начиная с TypeScript 4.4 при включённом strict: true переменная получает тип unknown:

try {
  // ...
} catch (error) {
  // error: unknown
  console.log(error.message); // Ошибка компилятора: Object is of type 'unknown'
}

Это правильное поведение — TypeScript не знает, что именно будет выброшено. В JavaScript можно бросить что угодно: throw "string", throw 42, throw { code: 500 }. Чтобы получить доступ к свойствам, нужно сначала сузить тип.

Сужение типа ошибки

Проверка instanceof

Самый распространённый способ — использование instanceof:

try {
  await fetchData();
} catch (error) {
  if (error instanceof Error) {
    console.log(error.message);
    console.log(error.stack);
  }
}

После проверки instanceof Error TypeScript знает, что error имеет тип Error со свойствами message, name, stack.

Обработка разных типов броска

Иногда бросают не объекты Error, а примитивы:

function riskyOperation() {
  if (Math.random() > 0.5) {
    throw "Something went wrong"; // строка
  }
  if (Math.random() > 0.5) {
    throw 42; // числовой код ошибки
  }
  throw new Error("Standard error");
}

try {
  riskyOperation();
} catch (error) {
  if (typeof error === "string") {
    console.log("Текстовая ошибка:", error.toUpperCase());
  } else if (typeof error === "number") {
    console.log("Код ошибки:", error);
  } else if (error instanceof Error) {
    console.log("Объект Error:", error.message);
  } else {
    console.log("Неизвестное исключение:", String(error));
  }
}

Пользовательский type guard

Для проверки произвольных объектов пишут type guard-функции:

function isError(value: unknown): value is Error {
  return value instanceof Error;
}

function hasMessage(value: unknown): value is { message: string } {
  return (
    typeof value === "object" &&
    value !== null &&
    "message" in value &&
    typeof (value as Record<string, unknown>).message === "string"
  );
}

try {
  // ...
} catch (error) {
  if (isError(error)) {
    console.log(error.message);
  } else if (hasMessage(error)) {
    console.log(error.message);
  } else {
    console.log("Неизвестная ошибка:", String(error));
  }
}

Пользовательские классы ошибок

Собственные классы ошибок добавляют контекст и позволяют различать типы ошибок через instanceof.

Создание иерархии

class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string
  ) {
    super(message);
    this.name = "AppError";
    // Нужно для корректной работы instanceof при компиляции в ES5
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class NetworkError extends AppError {
  constructor(
    message: string,
    public readonly statusCode: number
  ) {
    super(message, "NETWORK_ERROR");
    this.name = "NetworkError";
    Object.setPrototypeOf(this, NetworkError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message, "VALIDATION_ERROR");
    this.name = "ValidationError";
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

Использование в сервисе

async function createUser(data: unknown): Promise<User> {
  if (!isValidUserData(data)) {
    throw new ValidationError("Некорректные данные пользователя", "email");
  }

  const response = await fetch("/api/users", {
    method: "POST",
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new NetworkError("Ошибка создания пользователя", response.status);
  }

  return response.json();
}

// В точке вызова
try {
  const user = await createUser(formData);
} catch (error) {
  if (error instanceof ValidationError) {
    showFieldError(error.field, error.message);
  } else if (error instanceof NetworkError) {
    if (error.statusCode === 401) {
      redirectToLogin();
    } else {
      showNotification(error.message);
    }
  } else if (error instanceof AppError) {
    logError(error.code, error.message);
  }
}

Паттерн Result

Иногда выбрасывать исключения неудобно — лучше вернуть результат явно. Этот подход позволяет типизировать ошибки в сигнатуре функции, не прибегая к try/catch на каждом уровне.

Реализация Result

type Success<T> = {
  ok: true;
  value: T;
};

type Failure<E> = {
  ok: false;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

function ok<T>(value: T): Success<T> {
  return { ok: true, value };
}

function fail<E>(error: E): Failure<E> {
  return { ok: false, error };
}

Функция с Result

async function fetchUser(id: string): Promise<Result<User, NetworkError>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return fail(new NetworkError("Пользователь не найден", response.status));
    }
    const user = await response.json();
    return ok(user);
  } catch {
    return fail(new NetworkError("Ошибка сети", 0));
  }
}

// Вызов без try/catch — явная проверка
const result = await fetchUser("123");

if (result.ok) {
  renderUser(result.value); // TypeScript знает: это User
} else {
  showError(result.error.message); // TypeScript знает: это NetworkError
}

Дискриминированные объединения для ошибок

Можно описать все возможные ошибки через дискриминированные объединения:

type ApiError =
  | { type: "not_found"; id: string }
  | { type: "unauthorized" }
  | { type: "validation"; fields: Record<string, string> }
  | { type: "server"; message: string };

type ApiResult<T> = Result<T, ApiError>;

async function getProfile(userId: string): Promise<ApiResult<UserProfile>> {
  // ...
}

const result = await getProfile("42");

if (!result.ok) {
  switch (result.error.type) {
    case "not_found":
      console.log(`Пользователь ${result.error.id} не найден`);
      break;
    case "unauthorized":
      redirectToLogin();
      break;
    case "validation":
      showValidationErrors(result.error.fields);
      break;
    case "server":
      logServerError(result.error.message);
      break;
  }
}

TypeScript проверит, что все варианты обработаны.

Тип never для исчерпывающей проверки

Для гарантии полного покрытия всех вариантов используют assertNever:

function assertNever(value: never): never {
  throw new Error(`Необработанный случай: ${JSON.stringify(value)}`);
}

function handleApiError(error: ApiError): string {
  switch (error.type) {
    case "not_found":
      return `Не найдено: ${error.id}`;
    case "unauthorized":
      return "Требуется авторизация";
    case "validation":
      return "Ошибки валидации";
    case "server":
      return error.message;
    default:
      return assertNever(error); // ошибка компиляции, если добавить новый тип без обработки
  }
}

Если добавить новый тип в ApiError без обработки в switch — TypeScript выдаст ошибку компиляции. Это особенно ценно в командной разработке: новый тип ошибки нельзя «забыть» обработать.

Утилита tryCatch

Полезная обёртка для конвертации промисов в Result:

async function tryCatch<T>(
  promise: Promise<T>
): Promise<Result<T, Error>> {
  try {
    const value = await promise;
    return ok(value);
  } catch (error) {
    if (error instanceof Error) {
      return fail(error);
    }
    return fail(new Error(String(error)));
  }
}

// Использование — никакого try/catch в бизнес-коде
const result = await tryCatch(fetch("/api/data").then(r => r.json()));

if (result.ok) {
  processData(result.value);
} else {
  logger.error(result.error.message);
}

Практический пример: слой сервиса

Посмотрим, как собрать всё вместе в реальном приложении:

// errors.ts
export class BaseError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly details?: unknown
  ) {
    super(message);
    this.name = this.constructor.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

export class NotFoundError extends BaseError {
  constructor(resource: string, id: string) {
    super(`${resource} с id="${id}" не найден`, "NOT_FOUND", { resource, id });
  }
}

export class ConflictError extends BaseError {
  constructor(message: string) {
    super(message, "CONFLICT");
  }
}

// user-service.ts
type UserServiceError = NotFoundError | ConflictError | BaseError;

class UserService {
  async findById(id: string): Promise<Result<User, NotFoundError>> {
    const user = await db.users.findOne({ id });
    if (!user) {
      return fail(new NotFoundError("User", id));
    }
    return ok(user);
  }

  async create(data: CreateUserDto): Promise<Result<User, UserServiceError>> {
    const existing = await db.users.findOne({ email: data.email });
    if (existing) {
      return fail(new ConflictError(`Email ${data.email} уже используется`));
    }

    try {
      const user = await db.users.create(data);
      return ok(user);
    } catch (error) {
      const message = error instanceof Error ? error.message : "Неизвестная ошибка";
      return fail(new BaseError(message, "DB_ERROR", { originalError: error }));
    }
  }
}

// controller.ts
const userService = new UserService();

app.get("/users/:id", async (req, res) => {
  const result = await userService.findById(req.params.id);

  if (!result.ok) {
    if (result.error instanceof NotFoundError) {
      return res.status(404).json({ error: result.error.message });
    }
    return res.status(500).json({ error: "Internal Server Error" });
  }

  res.json(result.value);
});

Лучшие практики

Включайте strict: true в tsconfig. Это активирует useUnknownInCatchVariables и заставляет правильно типизировать ошибки в catch-блоках.

Всегда сужайте тип в catch. Никогда не обращайтесь к свойствам error без проверки через instanceof или type guard.

Используйте Object.setPrototypeOf в кастомных ошибках. Это нужно для корректной работы instanceof при компиляции в ES5.

Предпочитайте Result для предсказуемых ошибок. Если функция может вернуть ошибку в рамках нормального потока — Result<T, E> делает это явным в типе. Исключения оставьте для непредвиденных ситуаций.

Используйте assertNever в switch. Это даёт гарантию исчерпывающей проверки на уровне компилятора при добавлении новых вариантов ошибок.

Создавайте иерархии ошибок. Базовый класс + специализированные подклассы позволяют добавлять контекст и гибко обрабатывать разные типы сбоев на разных уровнях приложения.

Для углублённого изучения TypeScript и отработки этих паттернов на практических задачах — пройдите курс TypeScript на PurpleSchool: https://purpleschool.ru/course/typescript?utm_source=knowledgebase&utm_medium=text&utm_campaign=typescript-error-handling

Стрелочка влевоТип функции и стрелочные функции в TypeScriptTypeScript ключевое слово declareСтрелочка вправо

Постройте личный план изучения Typescript до уровня Middle — бесплатно!

Typescript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Typescript

Переменные и константы в TypeScriptTypeScript: ключевое слово using и Explicit Resource ManagementНастройка типов с declare module в TypeScriptTypeScript типизация в Node.js проектеTypeScript ESLint — настройка линтера для проектаTypeScript 5: новые возможности и измененияПреобразование к типу в TypeScript (Type Assertion)Кортежи в TypeScripttsconfig.json — полный разбор настроек TypeScript компилятораTypeScript с tRPC — типобезопасный APITypeScript strict и strictNullChecks — строгий режимКак собрать TypeScript проект с RollupОбъекты в TypeScriptMonorepo с Turborepo и TypeScriptФункции в TypeScriptТип функции и стрелочные функции в TypeScriptТипизация ошибок и обработка исключений в TypeScriptTypeScript ключевое слово declareМассивы в TypeScript
Как правильно использовать тип UnknownИспользование Union в TypeScriptTypeScript с GraphQL — типизация и кодогенерацияТипы данных в TypeScriptType Guards в TypeScript — как работает сужение типовSymbols в TypeScriptСтруктурная типизация в TypeScriptNull и Undefined в TypeScriptТип never в TypeScriptСужение типов (Narrowing) в TypeScriptПонимание Literal Types. Ключевые аспекты и примеры использованияОператор infer в Typescript?Типы функций в TypeScript. Руководство для разработчиковTypeScript с Express.js: типизация маршрутов и middlewareEnums в TypeScriptДискриминированные объединения в TypeScriptСлужебный тип Awaited в TypeScriptКак типизировать массивы TypeScriptКак правильно использовать тип any
Открыть базу знаний

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

CSS Flexbox

Антон Ларичев
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
бесплатно
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий