Однажды при работе с крупной кодовой базой одного фронтенд-приложения я заметил, что функционал постепенно группируется относительно команд (доменов). Каждая из таких групп функционала постепенно накладывает собственные ограничения на архитектуру. Как оказалось, обработка ошибок при сравнении кода двух разных команд неоднородна. В одном случае разработчики структурировали ошибки стандартным наследованием JS/TS, в другом были использованы перехваты возникающих ошибок и логирование.
Стало ясно, что нам требуется обобщить подход к тому, как мы структурируем (называем, наследуем) и выбрасываем ошибки. Как показала практика, соглашений о кодировании недостаточно.
Что мы хотели получить?
- Единый и строгий способ создания иерархии ошибок (от базовой к конечной функциональности)
- Возможность описывать отношение ошибки к контексту команды
- Обобщить механизм логирования в Sentry и повысить читабельность ошибок при работе с системой трекинга
- Обеспечить удобный API для передачи дополнительных параметров
Отказ от классов
“Good primitive is more than a framework”
Reatom Zen
Классы по умолчанию не накладывают ограничений на варианты и глубину наследования. Поэтому conway-errors предлагает в качестве программного API фабрику для создания иерархии ошибок.
Мы пришли к следующей схеме:
┌─────────────────────────────────────────────┐
│ createError([types]) │
│ │ │
│ ▼ │
│ ErrorContext │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ .subcontext() .subcontext() .feature() │
│ AuthError APIError UserAction │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ .feature() .feature() Error() │
│ OauthError PaymentAPI │
│ │ │ │
│ ▼ ▼ │
│ Error() Error() │
└─────────────────────────────────────────────┘
createError— единый корень для определения типов ошибок и конфигурации (контекст).subcontext()— возможность определить сколько угодно вложенных контекстов.feature()— создание конечной ошибки
Пример:
import { createError } from "conway-errors";
// Конфигурация и описание базовых типов возможных ошибок
const createErrorContext = createError([
{ errorType: "FrontendLogicError" },
{ errorType: "BackendLogicError" },
] as const);
// Создание корневого контекста
const errorContext = createErrorContext("MyProject");
// Создание подконтекстов
const apiErrorContext = errorContext.subcontext("APIError");
const authErrorContext = errorContext.subcontext("AuthError");
// Создание конкретных ошибок
const oauthError = authErrorContext.feature("OauthError");
const apiPaymentError = apiErrorContext.feature("APIPaymentError");
// Выброс ошибок
throw oauthError("FrontendLogicError", "Пользователь не найден");
// Результат: "FrontendLogicError: MyProject/AuthError/OauthError: Пользователь не найден"
throw apiPaymentError("BackendLogicError", "Платеж уже обработан");
// Результат: "BackendLogicError: MyProject/APIError/APIPaymentError: Платеж уже обработан"
Единая точка конфигурации
Созданные ошибки при помощи conway-errors — просто объекты. Вы сами решаете, что с ними делать:
import { createError } from "conway-errors";
const createErrorContext = createError([
{ errorType: "ValidationError" },
{ errorType: "NetworkError" },
] as const);
const appErrors = createErrorContext("MyApp");
const loginError = appErrors.feature("LoginError");
// Вариант 1: выбрасывание через стандартный throw e;
throw loginError("ValidationError", "Неверный формат email");
// Вариант 2: логирование без выброса (по умолчанию console.error())
loginError("ValidationError", "Неверный формат email").emit();
Таким образом, примитив позволяет вам самостоятельно выбрать слой в вашем приложении для выброса и перехвата. Одна из наших ключевых идей была определить интеграцию с логированием в Sentry «наверху»:
import { createError } from "conway-errors";
import * as Sentry from "@sentry/nextjs";
const createErrorContext = createError([
{ errorType: "FrontendLogicError" },
{ errorType: "BackendLogicError" }
] as const, {
// Пользовательская обработка ошибок для мониторинга
handleEmit: (err) => {
Sentry.captureException(err);
},
});
const appErrors = createErrorContext("MyApp");
const userError = appErrors.feature("UserAction");
// Автоматически логирует в <a href="https://sentry.io" target="_blank" rel="noopener">Sentry</a> при использовании emit()
userError("FrontendLogicError", "Валидация формы не прошла").emit();
Добавление расширенных параметров
На разных уровнях приложения могут быть необходимые данные, которые могут пригодиться для выброса и логирования конечной ошибки.
Важная особенность всего API библиотеки — это возможность указать extendedParams во всех методах.
Важно: Вложенные контексты и feature перезаписывают верхние параметры (extendedParams)
Лучше всего продемонстрировать на примере:
import { createError } from "conway-errors";
import * as Sentry from "@sentry/nextjs";
const createErrorContext = createError(
["FrontendLogicError", "BackendLogicError"],
{
extendedParams: {
environment: process.env.NODE_ENV,
version: "1.2.3"
},
}
);
const paymentErrors = createErrorContext("Payment", {
extendedParams: { service: "stripe" }
});
const cardPayment = paymentErrors.feature("CardPayment", {
extendedParams: { region: "us-east-1" }
});
const error = cardPayment("BackendLogicError", "Сбой обработки платежа");
error.emit({
extendedParams: {
userId: "user-123",
action: "checkout",
severity: "critical"
}
});
Разбиение на домены
“Любая организация, которая разрабатывает систему (в широком смысле), вынуждена создавать проекты, структуры которых являются копией структуры связей организации.”
Закон Конвея, Википедия
Если ваш проект разрабатывает несколько команд и код начал разбиваться на разные поддомены, conway-errors предоставляет вам несколько вариантов структурирования в силу своего гибкого API. Вы можете выбрать наиболее удобный вариант для вашего проекта.
- Корневой контекст для каждой команды
import { createError } from "conway-errors";
const createErrorContextPaymentTeamErrorContext = createError([
{ errorType: "BackendLogicError" },
] as const);
const createAuthTeamErrorContext = createError([
{ errorType: "FrontendLogicError" },
{ errorType: "BackendLogicError" },
] as const);
// определяете для каждой команды свои подконтексты
// ...
- Подконтексты с параметрами для каждой команды
import { createError } from "conway-errors";
// единый корневой контекст
const createErrorContext = createError([
{ errorType: "FrontendLogicError" },
{ errorType: "BackendLogicError" },
] as const);
// Использование extendedParams для атрибуции команды (рекомендуется)
const authErrors = projectErrors.subcontext("Auth", {
extendedParams: { team: "Auth Team" }
});
const paymentErrors = projectErrors.subcontext("Payment", {
extendedParams: { team: "Payment Team" }
});
- Подконтексты для каждой команды
import { createError } from "conway-errors";
// единый корневой контекст
const createErrorContext = createError([
{ errorType: "FrontendLogicError" },
{ errorType: "BackendLogicError" },
] as const);
const authErrors = createErrorContext("Auth Team");
const paymentErrors = createErrorContext("Payment Team");
Не забывайте о том, что вы можете попробовать придумать свой метод атрибуции команды (или поддомена).
Заключение
После перехода на conway-errors мы добились того же поведения меньшим количеством кода. Дополнительно мы обобщили создание и структурирование ошибок для всех команд в проекте.

Приглашаю в репозиторий проекта, устанавливайте и пробуйте! Если у вас есть идеи по улучшению библиотеки — поделитесь в GitHub Issues!