Паттерн Result для обработки ошибок без исключений

16 октября 2026
Автор

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

Проблема с исключениями в TypeScript

Типичный TypeScript-код использует try/catch для обработки ошибок. На первый взгляд это удобно, но у подхода есть фундаментальный изъян: сигнатура функции не сообщает вызывающей стороне, что та может выбросить исключение.

// Ничего не говорит о том, что здесь может произойти ошибка
function parseUser(json: string): User {
  return JSON.parse(json); // может выбросить SyntaxError
}

// Компилятор не заставит вас обрабатывать ошибку
const user = parseUser(rawInput); // потенциальный краш
console.log(user.name);

Типы TypeScript описывают путь успеха, но молчат о пути ошибки. Это нарушает контракт функции и делает код хрупким.

Паттерн Result решает эту проблему: ошибка становится частью возвращаемого типа, и компилятор заставляет вас её обработать.

Курс по теме

Курс TypeScript с нуля с нуля

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

Что такое паттерн Result

Result — это тип-объединение (discriminated union), который представляет либо успешный результат (Ok), либо ошибку (Err). Такой подход пришёл из функциональных языков — Rust, Haskell, F# — и отлично ложится на систему типов TypeScript.

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

Функция, возвращающая Result<User, ParseError>, явно сигнализирует: «я могу вернуть либо пользователя, либо ошибку разбора». Вызывающая сторона обязана обработать оба варианта.

Базовая реализация

Начнём с минимального набора: тип и два конструктора.

type Result<T, E = Error> =
  | { 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 };
}

Теперь перепишем функцию разбора JSON:

type ParseError = { message: string; raw: string };

function parseUser(json: string): Result<User, ParseError> {
  try {
    const data = JSON.parse(json);
    return Ok({ name: data.name, age: data.age });
  } catch {
    return Err({ message: 'Invalid JSON', raw: json });
  }
}

// Теперь TypeScript требует обработки обоих случаев
const result = parseUser(rawInput);

if (result.ok) {
  console.log(result.value.name); // TypeScript знает тип здесь
} else {
  console.error(result.error.message); // и здесь
}

Обратите внимание: внутри ветки if (result.ok) TypeScript автоматически сужает тип до { ok: true; value: User }, а в ветке else — до { ok: false; error: ParseError }. Это работает благодаря discriminated union.

Типизация ошибок

Одно из главных преимуществ паттерна — возможность точно описать, какие ошибки может вернуть функция.

type ValidationError = {
  kind: 'validation';
  field: string;
  message: string;
};

type NotFoundError = {
  kind: 'not_found';
  id: string;
};

type DatabaseError = {
  kind: 'database';
  message: string;
};

type UserServiceError = ValidationError | NotFoundError | DatabaseError;

async function getUser(id: string): Promise<Result<User, UserServiceError>> {
  if (!id.match(/^[a-z0-9-]+$/)) {
    return Err({ kind: 'validation', field: 'id', message: 'Invalid format' });
  }

  const user = await db.findById(id);

  if (!user) {
    return Err({ kind: 'not_found', id });
  }

  return Ok(user);
}

Теперь на месте вызова можно исчерпывающе обработать все варианты ошибок:

const result = await getUser(userId);

if (!result.ok) {
  switch (result.error.kind) {
    case 'validation':
      return res.status(400).json({ error: result.error.message });
    case 'not_found':
      return res.status(404).json({ error: 'User not found' });
    case 'database':
      logger.error(result.error.message);
      return res.status(500).json({ error: 'Internal error' });
  }
}

return res.json(result.value);

Если добавить новый тип ошибки в UserServiceError и забыть его обработать — TypeScript сообщит об ошибке компиляции.

Цепочки операций: map и flatMap

Одна функция, возвращающая Result, — это хорошо. Но в реальных задачах нужно последовательно применять несколько операций, каждая из которых может завершиться ошибкой. Без вспомогательных методов код превращается в лесенку if:

const parseResult = parseJson(raw);
if (!parseResult.ok) return parseResult;

const validateResult = validateSchema(parseResult.value);
if (!validateResult.ok) return validateResult;

const saveResult = await saveToDb(validateResult.value);
if (!saveResult.ok) return saveResult;

Методы map и flatMap позволяют выстроить цепочку читаемо.

map — преобразование успешного значения

map применяет функцию к значению внутри Ok, не трогая Err:

function map<T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => U
): Result<U, E> {
  if (result.ok) {
    return Ok(fn(result.value));
  }
  return result;
}

// Пример
const lengthResult = map(parseUser(raw), (user) => user.name.length);
// Result<number, ParseError>

flatMap — цепочка операций, возвращающих Result

flatMap применяет функцию, которая сама возвращает Result, и разворачивает вложенность:

function flatMap<T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => Result<U, E>
): Result<U, E> {
  if (result.ok) {
    return fn(result.value);
  }
  return result;
}

Сложим цепочку из начала раздела:

const result = flatMap(
  flatMap(parseJson(raw), validateSchema),
  (data) => saveToDb(data)
);

Ещё чище — через класс с fluent API:

class ResultWrapper<T, E> {
  constructor(private readonly inner: Result<T, E>) {}

  map<U>(fn: (value: T) => U): ResultWrapper<U, E> {
    return new ResultWrapper(map(this.inner, fn));
  }

  flatMap<U>(fn: (value: T) => Result<U, E>): ResultWrapper<U, E> {
    return new ResultWrapper(flatMap(this.inner, fn));
  }

  unwrap(): Result<T, E> {
    return this.inner;
  }
}

function result<T, E>(r: Result<T, E>) {
  return new ResultWrapper(r);
}

// Использование
const final = result(parseJson(raw))
  .flatMap(validateSchema)
  .flatMap(enrichWithDefaults)
  .map((data) => ({ ...data, processedAt: new Date() }))
  .unwrap();

Асинхронный Result

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

type AsyncResult<T, E = Error> = Promise<Result<T, E>>;

async function tryCatch<T, E = Error>(
  fn: () => Promise<T>,
  mapError: (error: unknown) => E
): AsyncResult<T, E> {
  try {
    return Ok(await fn());
  } catch (error) {
    return Err(mapError(error));
  }
}

Теперь любую async-функцию можно безопасно обернуть:

const result = await tryCatch(
  () => fetch('/api/users').then((r) => r.json()),
  (err) => ({ kind: 'network' as const, message: String(err) })
);

if (result.ok) {
  console.log(result.value);
}

Для асинхронных цепочек удобна функция andThen:

async function andThen<T, U, E>(
  result: AsyncResult<T, E>,
  fn: (value: T) => AsyncResult<U, E>
): AsyncResult<U, E> {
  const r = await result;
  if (r.ok) {
    return fn(r.value);
  }
  return r;
}

// Использование
const response = await andThen(
  andThen(
    fetchUserData(userId),
    (data) => validateUserData(data)
  ),
  (user) => saveUser(user)
);

Практический пример: валидация формы

Паттерн Result хорошо подходит для валидации входных данных:

type FieldError = { field: string; message: string };

function validateEmail(email: string): Result<string, FieldError> {
  if (!email.includes('@')) {
    return Err({ field: 'email', message: 'Некорректный email' });
  }
  return Ok(email.toLowerCase());
}

function validateAge(age: number): Result<number, FieldError> {
  if (age < 18 || age > 120) {
    return Err({ field: 'age', message: 'Возраст должен быть от 18 до 120' });
  }
  return Ok(age);
}

type RegistrationData = { email: string; age: number };

function validateRegistration(
  input: Record<string, unknown>
): Result<RegistrationData, FieldError> {
  const emailResult = validateEmail(String(input.email ?? ''));
  if (!emailResult.ok) return emailResult;

  const ageResult = validateAge(Number(input.age));
  if (!ageResult.ok) return ageResult;

  return Ok({ email: emailResult.value, age: ageResult.value });
}

// На месте вызова
const validation = validateRegistration(formData);

if (!validation.ok) {
  showFieldError(validation.error.field, validation.error.message);
} else {
  submitRegistration(validation.value);
}

Сравнение с try/catch

Критерий try/catch Result
Видимость в типах Нет Да
Принудительная обработка Нет Да (через narrowing)
Читаемость цепочек Низкая Высокая (с map/flatMap)
Производительность Лучше Незначительно хуже
Совместимость с legacy Да Требует адаптации

Исключения остаются уместными для действительно непредвиденных ситуаций: ошибок программирования, выхода за пределы массива, нехватки памяти. Result подходит для ожидаемых исходов бизнес-логики: «пользователь не найден», «невалидные данные», «сетевой таймаут».

Паттерн unwrapOr для значений по умолчанию

Иногда нужно получить значение или запасной вариант, не разворачивая Result вручную:

function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
  return result.ok ? result.value : defaultValue;
}

function unwrapOrElse<T, E>(result: Result<T, E>, fn: (error: E) => T): T {
  return result.ok ? result.value : fn(result.error);
}

// Примеры
const config = unwrapOr(loadConfig('./config.json'), defaultConfig);

const user = unwrapOrElse(
  parseUser(raw),
  (err) => { logger.warn(err.message); return guestUser; }
);

Когда использовать паттерн Result

Result оправдан, когда:

  • Ошибка является частью бизнес-логики (не найдено, не авторизовано, невалидно)
  • Функция вызывается в цепочке, и каждый шаг может прервать её
  • Важно, чтобы вызывающий код не мог «забыть» обработать ошибку
  • Вы работаете в функциональном стиле и хотите составлять операции

Оставьте try/catch для:

  • Третьесторонних библиотек, которые выбрасывают исключения
  • Глобальных обработчиков на верхнем уровне
  • Ошибок программирования (TypeError, RangeError)

На практике оба подхода сосуществуют: try/catch внутри функции адаптирует исключение в Result, который возвращается наружу.


Чтобы глубже разобраться с системой типов TypeScript, дискриминированными объединениями и продвинутыми паттернами разработки — изучите курс TypeScript на PurpleSchool.

Стрелочка влевоОператор satisfies в TypeScriptУтилитный тип NoInfer в TypeScriptСтрелочка вправо

Постройте личный план изучения 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 — строгий режимБезопасные env переменные в TypeScriptКак собрать TypeScript проект с RollupMonorepo с Turborepo и TypeScriptОбъекты в TypeScriptTypeScript с Hono: типобезопасный веб-фреймворкФункции в 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 с Express.js: типизация маршрутов и middlewareТипы функций в TypeScript. Руководство для разработчиковEnums vs Union Types в TypeScript: сравнение и выборEnums в TypeScriptДискриминированные объединения в TypeScriptСлужебный тип Awaited в TypeScriptКак типизировать массивы TypeScriptКак правильно использовать тип any
Вариативные кортежи (Variadic Tuple Types) в TypeScriptUtility Types в TypeScript — полный разбор встроенных утилитТипизация ответов API с fetch и generic-функцииtypeof в TypeScriptTemplate Literal Types в TypeScriptОператор satisfies в TypeScriptПаттерн Result для обработки ошибок без исключенийУтилитный тип NoInfer в TypeScriptMapped Types в TypeScriptОператор типа keyof в TypeScriptРуководство по тестированию TypeScript с JestТипы доступа по индексу в TypeScriptGenerics в TypeScriptconst assertions в TypeScriptУсловные типы в TypeScriptBranded Types в TypeScript — номинальная типизацияПродвинутые Conditional Types в TypeScript
Открыть базу знаний

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

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

TypeScript с нуля

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

CSS Flexbox

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

Next.js - с нуля

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

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