Перейти к основному содержимому

Обработка ошибок в TypeScript

Разработчику Архитектору

Дальше: Паттерны · TypeScript и Node.js · Асинхронность · Справочник — распространённые паттерны


Механика throw в TypeScript та же, что в JavaScript. Зато типы помогают описать ошибки явно: ожидаемые сбои удобно класть в Result, инфраструктурные — обрабатывать в catch с unknown. Так важные ветки обработки видны в контракте, а не теряются в общем try/catch.

Маршрут: АсинхронностьошибкиПаттерныTypeScript и Node.js.

unknown в catch — 10.md, 17.md. Валидация — 6.md.


Два семейства ошибок

ТипПримерыКак моделировать
Ожидаемыеневалидный email, ресурс не найденResult, union, HTTP 4xx
Неожиданныедиск, OOM, багthrow, HTTP 5xx, лог + alert
// Ожидаемая — часть контракта
function parseAge(input: string): Result<number, "nan" | "negative"> { /* ... */ }

// Неожиданная — прерывает поток
async function readSecret(): Promise<string> {
const data = await fs.readFile("/secret", "utf8");
return data;
}

Result<T, E>

type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };

function parseAge(input: string): Result<number, "nan" | "negative"> {
const value = Number(input);
if (Number.isNaN(value)) return { ok: false, error: "nan" };
if (value < 0) return { ok: false, error: "negative" };
return { ok: true, value };
}

function displayAge(input: string): string {
const r = parseAge(input);
if (!r.ok) {
return r.error === "nan" ? "Введите число" : "Возраст не может быть отрицательным";
}
return `Возраст: ${r.value}`;
}

Разбор:

  • Вызывающий обязан проверить ok — иначе нет доступа к value.
  • E можно сузить до union литералов — автодополнение в if (!r.ok).

Комбинирование Result

function mapResult<T, U, E>(
r: Result<T, E>,
fn: (v: T) => U,
): Result<U, E> {
return r.ok ? { ok: true, value: fn(r.value) } : r;
}

В библиотеках: neverthrow, ts-results — по желанию команды.


Option<T> (отсутствие значения)

type Option<T> = { tag: "some"; value: T } | { tag: "none" };

function findById<T extends { id: string }>(
items: T[],
id: string,
): Option<T> {
const item = items.find((i) => i.id === id);
return item ? { tag: "some", value: item } : { tag: "none" };
}

function getName(items: User[], id: string): string {
const found = findById(items, id);
return found.tag === "some" ? found.value.name : "Гость";
}

Разбор:

  • Альтернатива — T | null с strictNullChecks11.md.
  • Option явно показывает в API, что значения может не быть.

try/catch и unknown

function toMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "Неизвестная ошибка";
}

async function load(): Promise<Result<string, "network">> {
try {
const res = await fetch("/api/data");
if (!res.ok) return { ok: false, error: "network" };
return { ok: true, value: await res.text() };
} catch (e: unknown) {
console.error(toMessage(e));
return { ok: false, error: "network" };
}
}

Разбор:

  • Не используйте catch (e: any).
  • instanceof Error не покрывает все throw в JS.

Кастомные классы ошибок

class AppError extends Error {
constructor(
message: string,
readonly code: "NOT_FOUND" | "FORBIDDEN" | "VALIDATION",
) {
super(message);
this.name = "AppError";
}
}

function isAppError(e: unknown): e is AppError {
return e instanceof AppError;
}

HTTP-маппинг в Node:

function toStatus(e: AppError): number {
switch (e.code) {
case "NOT_FOUND":
return 404;
case "FORBIDDEN":
return 403;
case "VALIDATION":
return 400;
}
}

Разбор:

  • Класс удобен для центрального error middleware.
  • Для чистых функций предпочтительнее Result без throw.

Ошибки в async-цепочках

async function fetchUser(id: string): Promise<Result<User, "http" | "invalid">> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return { ok: false, error: "http" };
const raw: unknown = await res.json();
if (!isUser(raw)) return { ok: false, error: "invalid" };
return { ok: true, value: raw };
} catch {
return { ok: false, error: "http" };
}
}

Promise reject vs возврат Result — выберите один стиль на слой; смешение усложняет код.


Единый формат API-ошибки

type ApiErrorBody = {
error: {
code: string;
message: string;
details?: Record<string, string[]>;
};
};

function errorResponse(
code: string,
message: string,
status: number,
): Response {
const body: ApiErrorBody = { error: { code, message } };
return new Response(JSON.stringify(body), { status });
}

Разбор:

  • Frontend типизирует ApiErrorBody и показывает details у полей формы.

Zod на границе

import { z } from "zod";

const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
});

type CreateUserDto = z.infer<typeof CreateUserSchema>;

function parseCreateUser(body: unknown): Result<CreateUserDto, string> {
const parsed = CreateUserSchema.safeParse(body);
return parsed.success
? { ok: true, value: parsed.data }
: { ok: false, error: parsed.error.message };
}

Подробнее — 6.md.


throw vs Result — шпаргалка

СитуацияРекомендация
Валидация формы / DTOResult или Zod safeParse
Ресурс не найден в сервисеResult или AppError NOT_FOUND
Падение диска / сеть без обработкиthrow + middleware
React load dataunion LoadState21.md

Частые ошибки

ОшибкаЧто делать
catch (e: any)unknown + narrowing
Игнорировать !r.okвключить ESLint на floating promises / strict
throw для валидации вездеResult на границе domain
Один Error на всёunion кодов или классы
Типы ошибок не в APIApiErrorBody

Практика

  1. Перепишите парсер числа на Result<number, "nan" | "negative">.
  2. Добавьте findById с Option или Result.
  3. Сделайте AppError + маппинг в HTTP-статус.
  4. В React отобразите LoadState error — 21.md.
  5. Оберните POST body в Zod safeParse (опционально).

Смежные статьи