Skip to content

conway-errors: порядок в ошибках как часть архитектуры проекта

Published:
GitHub NPM

Однажды при работе с крупной кодовой базой одного фронтенд-приложения я заметил, что функционал постепенно группируется относительно команд (доменов). Каждая из таких групп функционала постепенно накладывает собственные ограничения на архитектуру. Как оказалось, обработка ошибок при сравнении кода двух разных команд неоднородна. В одном случае разработчики структурировали ошибки стандартным наследованием JS/TS, в другом были использованы перехваты возникающих ошибок и логирование.

Стало ясно, что нам требуется обобщить подход к тому, как мы структурируем (называем, наследуем) и выбрасываем ошибки. Как показала практика, соглашений о кодировании недостаточно.

Что мы хотели получить?

  1. Единый и строгий способ создания иерархии ошибок (от базовой к конечной функциональности)
  2. Возможность описывать отношение ошибки к контексту команды
  3. Обобщить механизм логирования в Sentry и повысить читабельность ошибок при работе с системой трекинга
  4. Обеспечить удобный 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()                    │
└─────────────────────────────────────────────┘

Пример:

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. Вы можете выбрать наиболее удобный вариант для вашего проекта.

  1. Корневой контекст для каждой команды
import { createError } from "conway-errors";

const createErrorContextPaymentTeamErrorContext = createError([
  { errorType: "BackendLogicError" },
] as const);

const createAuthTeamErrorContext = createError([
  { errorType: "FrontendLogicError" },
  { errorType: "BackendLogicError" },
] as const);

// определяете для каждой команды свои подконтексты
// ...
  1. Подконтексты с параметрами для каждой команды
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" }
});
  1. Подконтексты для каждой команды
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 мы добились того же поведения меньшим количеством кода. Дополнительно мы обобщили создание и структурирование ошибок для всех команд в проекте.

Conway Errors Changes

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

GitHub NPM