Однажды при работе с крупной кодовой базой одного фронтенд-приложения я заметил, что функционал постепенно группируется относительно команд (доменов). Каждая из таких групп функционала постепенно накладывает собственные ограничения на архитектуру. Как оказалось, обработка ошибок при сравнении кода двух разных команд неоднородна. В одном случае разработчики структурировали ошибки стандартным наследованием 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!
