Обработка ошибок в TypeScript
Дальше: Паттерны · TypeScript и Node.js · Асинхронность · Справочник — распространённые паттерны
Механика throw в TypeScript та же, что в JavaScript. Зато типы помогают описать ошибки явно: ожидаемые сбои удобно класть в Result, инфраструктурные — обрабатывать в catch с unknown. Так важные ветки обработки видны в контракте, а не теряются в общем try/catch.
Маршрут: Асинхронность → ошибки → Паттерны → TypeScript и Node.js.
Два семейства ошибок
| Тип | Примеры | Как моделировать |
|---|---|---|
| Ожидаемые | невалидный 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сstrictNullChecks— 11.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 — шпаргалка
| Ситуация | Рекомендация |
|---|---|
| Валидация формы / DTO | Result или Zod safeParse |
| Ресурс не найден в сервисе | Result или AppError NOT_FOUND |
| Падение диска / сеть без обработки | throw + middleware |
| React load data | union LoadState — 21.md |
Частые ошибки
| Ошибка | Что делать |
|---|---|
catch (e: any) | unknown + narrowing |
Игнорировать !r.ok | включить ESLint на floating promises / strict |
| throw для валидации везде | Result на границе domain |
Один Error на всё | union кодов или классы |
| Типы ошибок не в API | ApiErrorBody |
Практика
- Перепишите парсер числа на
Result<number, "nan" | "negative">. - Добавьте
findByIdсOptionилиResult. - Сделайте
AppError+ маппинг в HTTP-статус. - В React отобразите
LoadStateerror — 21.md. - Оберните
POSTbody в ZodsafeParse(опционально).