js-base-error
v0.6.0
Published
JS Base Error.
Readme
💥 JS Base Error
Унифицированная и безопасная система ошибок для TypeScript.
npm i js-base-error- Базовый класс
BaseError(наследует стандартнуюErrorиErrorLike). - ⚡Молниеносная ошибка без захвата стека
LiteError(наследуетErrorLike). - Декларативное определение полей(
nameи т.п.) для вывода в JSON или строку. - Безопасное преобразование к JSON (
errorToJsonLike). - Форматирование к человекочитаемому многострочному тексту (
errorToString). - Простое расширение через наследование (
class MyError extends BaseError).
- 🔥 Использование
- 🛠️ Внутренний механизм и зарезервированное поле
detail - 📌 Резервное поле
__metaдля JSON - ⚙️ Требования к окружению и компиляции
- 👇 Использование в зависимых библиотеках
Совет: Используй LiteError вместо Error. Нативная ошибка полезна только для отладки и трассировки - во всех остальных случаях LiteError быстрее, безопаснее и предсказуемее. bench.js:
LiteError (ErrorLike) - chromium
249.97x faster than Error (native)
281.48x faster than BaseError (native + ErrorLike)🔥 Использование
import {
type TNullish,
type TJsonLike,
type TErrorLevel,
type IErrorDetail,
type IErrorLike,
type IErrorCollection,
type TMetaValue,
type TMetaTruncated,
type TMetaPlaceholder,
type TSerializationOptions,
SerializationParameters,
ensureSerializationParameters,
captureStackTrace,
defineErrorLike,
ErrorLike,
LiteError,
BaseError,
ErrorCollection,
isErrorLike,
errorToJsonLike,
errorToString
} from 'js-base-error'🚀 Быстрый старт
Определи базовые ошибки приложения:
class AppError extends BaseError {
// Все перечислимые свойства класса на любой иерархии - будут скопированы.
override name = 'AppError'
code = 'E0058'
constructor(detail: IErrorDetail) {
super(detail)
}
}
const error = new AppError({ message: 'Oh no 😮', cause: { reason: '🕷️' } })Приведи ошибку к форматированной строке:
const str = error.toString()name: AppError
message: Oh no 😮
code: E0058
cause:
reason: 🕷️Используй функции errorToJsonLike(...) и errorToString(...) или передай параметры ограничений в расширенные методы toJsonWith(...) и toStringWith(...):
const json = error.toJsonWith({ includeStack: true })
{
name: 'AppError',
message: 'Oh no 😮',
code: 'E0058',
cause: { reason: '🕷️' },
stack: 'Error: ...'
}LiteError обеспечивает тот же API, но не захватывает stack(высокая скорость и низкая стоимость создания):
(error instanceof ErrorLike) // true
(new LiteError() instanceof ErrorLike) // true⚙️ Конфигурация ограничений вывода полей JSON
Конфигурация ограничений позволяет контролировать глубину и объем сериализуемых данных при логировании или экспорте ошибок. Это особенно важно при работе с большими объектами, вложенными коллекциями и рекурсивными структурами(рекурсия игнорируется). Настройка ограничений предотвращает раздувание логов и защищает систему от случайного вывода чувствительных данных.
Опции TSerializationOptions позволяют задать лимиты длины строк, количества элементов и исключить поля по имени. Это гарантирует компактный и безопасный формат даже при сложных ошибках.
Параметры ограничений SerializationParameters и TSerializationOptions можно передать для каждого запроса или установить глобальную конфигурацию:
SerializationParameters.configure({
maxItems: 2,
maxTotalItems: 10,
maxStringLength: 8,
exclude: ['token'], // запрещаем вывод
// ... и еще несколько опций
})Теперь вызов методов toJson()/toJSON()/toJsonWith()/toString()/toStringWith() и функций errorToJsonLike()/errorToString() используют эти параметры по умолчанию без повторной инициализации.
🎲 Агрегирование ошибок
ErrorCollection объединяет ошибки и выводит коллекцию как одно поле(внутри другой ошибки) или как самостоятельный массив:
const combined = new ErrorCollection([
new LiteError({ message: '123456789', level: 'debug' })
])
combined.push({ name: 'TokenError', token: 'private' })
const list = combined.toStringWith(/* global */)Результат с ограничением полей:
[0]:
name: LiteError
message: 12345678
__meta:
kind: object
total: 3
truncated: 1
[1]:
name: TokenErrorЭта ошибка состоит из нескольких ошибок, коллекции и еще много чего
throw new CosmicRayFluxError()Посмотреть на нее можно здесь errors.test.ts
name: CosmicRayFluxError
message: A high-energy particle corrupted a critical memory address.
code: CRF-001
stack: CosmicRayFluxError: A high-energy particle...
at processTransaction (/app/services/payment.js:123:45)
at handleRequest (/app/server.js:80:10)
cause:
name: DatabaseTimeoutError
message: Query timed out while fetching user data
query: SELECT * FROM users;
timeout: 3000
level: fatal
timestamp:
__meta:
type: date
value: 2025-12-25T13:37:00.000Z
transactionId:
__meta:
type: bigint
value: 123456789012345678901234567890
validator:
__meta:
type: regexp
value: /^[a-zA-Z0-9]+\\n$/
affectedSystem:
service: auth-service
cpuCore: 7
isCritical: true
failedAttempts:
[0]: 1
[1]: two
[2]:
__meta:
type: symbol
value: Symbol(three)
subsystemFailures:
[0]:
__meta:
kind: error
name: CacheMissError
message: Session data not found in Redis
[1]:
__meta:
kind: error
name: MetricsError
message: Failed to report to Prometheus🛠️ Внутренний механизм и зарезервированное поле detail
Основой js-base-error является концепция ленивой инициализации и декларативного определения полей ошибки. Это позволяет легко создавать иерархии ошибок, где свойства определяются прямо в классах, а вся сложная работа по их сбору происходит автоматически и только при необходимости.
Ключевую роль в этом играет свойство detail.
Как работает detail: от Геттера к Свойству
⚠️ Важно: Приватное поле
_detailзарезервировано для хранения ссылки на объект переданный конструктору.
При создании экземпляра ошибки (new AppError(...)), свойство detail на самом деле является геттером, унаследованным от прототипа ErrorLike.
- Ленивая инициализация: Вся "магия" происходит только в момент первого обращения к
error.detail. До этого момента не производится никаких дорогостоящих операций по сбору свойств. Это полезно в сценариях, где ошибка создается, но ее детали могут никогда не понадобиться. - Сбор свойств: При первом вызове геттер
detailзапускает внутренний механизм (captureErrorProperties), который сканирует экземпляр ошибки и всю его цепочку прототипов. Он собирает все собственные перечислимые свойства (enumerable: true), которые находит на своем пути. - Правило приоритетов: При сборке свойств действует простое правило: значения из объекта
_detail, переданного в конструктор, имеют наивысший приоритет. Если передатьnew AppError({ name: 'Overridden' }), это значение будет использовано, даже если в классеAppErrorопределеноoverride name = 'AppError'. Это позволяет гибко переопределять стандартные поля для каждого конкретного случая. - Мемоизация (Замещение геттера): Сразу после того, как все свойства собраны в единый объект(приватный
_detail), геттерdetailна экземпляре ошибки замещается простым свойством, значением которого становится этот собранный объект. Все последующие обращения кerror.detailбудут мгновенно возвращать этот объект, минуя всю логику инициализации. Это можно проверить с помощьюObject.hasOwn(error, 'detail'), который вернетfalseдо первого доступа иtrueпосле.
Зарезервированные поля:
get detail()- Свойство прототипа с ленивым инициализатором. Всегда возвращает объект, даже если не были переданы параметры._detail- Приватное(protected) поле данных. Принадлежит инстансу для хранения объекта с деталями ошибки, который передается конструктору. Кто определяет это поле не имеет значения, но именно его будет читать инициализатор. Если это объект{}- ссылка используется как есть(возвращается черезdetail), иначе игнорируется.toString()/toJson()/toJSON()/toStringWith()/toJsonWith()- Методы форматирования.
Доступ ко всем функциям библиотеки:
import { inspectAny, ... } from 'js-base-error/dev' // <- dev
const [type, value] = inspectAny(anyValue, ...)Иерархия ошибок приложения
Чаще всего ошибки не имеют сложных методов, но для примера определим поле, синхронизирующее доступ к detail через перечислимое свойство-аксессор:
// Расширим стандартные поля для TS и автоподсказок в IDE
interface ILibErrorDetail extends IErrorDetail {
features?: string
}
// Базовая ошибка библиотеки
class LibError extends BaseError<ILibErrorDetail> {
override readonly name: string = 'LibError'
readonly lib = 'lib_v5.04.85.test'
// Такое необходимо для defineProperty
declare features: string
}
// Синхронизация features с detail
Object.defineProperty(LibError.prototype, 'features', {
enumerable: true,
// Обращаться нужно именно к detail. Беспокоится о круговых
// ссылках не стоит - об этом позаботится инициализатор.
get () { return this.detail.features ?? 'default' },
set (v) { this.detail.features = v }
})
// Конкретизированные ошибки
class ConcreteError extends LibError {
// Переопределяет базовое имя
override readonly name = 'ConcreteError'
}Значения параметров IErrorDetail имеют приоритет и перезапишут любое поле по умолчанию:
const error1 = new ConcreteError({ features: 'custom' })
const error2 = new ConcreteError({ name: 'RenamedError' })
// Поле `detail` инициализируется после первого вызова
Object.hasOwn(error1, 'detail') // false
error1.detail // ===
{
name: 'ConcreteError',
lib: 'lib_v5.04.85.test',
features: 'custom'
}
error2.detail // ===
{
name: 'RenamedError',
lib: 'lib_v5.04.85.test',
features: 'default'
}
Object.hasOwn(error1, 'detail') // trueОграничения:
Поля данных(переопределенные name и lib), собираемые в detail, не могут быть синхронизированы и, в первую очередь, предназначены для декларативного определения константных значений ошибки. Значения таких полей, собираются один раз, и последующие изменения доступны только через detail.
Рекомендации
Нет функциональной разницы между BaseError, LiteError и любым пользовательским классом, расширяющим ErrorLike. Все они обладают единым интерфейсом и одинаковым поведением при сериализации, форматировании и проверке типов (error instanceof ErrorLike).
Разница лишь в том, нужен ли приложению стек вызовов:
- Используй
BaseError, если стек помогает при отладке, тестировании или трассировке. - Используй
LiteError, если ошибка создаётся часто (валидация, отмена, таймаут, ...) и стек не имеет смысла. - Для гибридных сценариев можно определить абстрактную базу, которая выбирает реализацию в зависимости от окружения (dev / prod).
Таким образом, выбор реализации не влияет на API, сериализацию и контракты, а лишь на стоимость создания и наличие стека.
В небольших и средних проектах удобно централизовать определения ошибок в отдельном модуле errors.ts. Такой подход позволяет:
- Хранить всю иерархию ошибок в одном месте.
- Быстро переключаться между
BaseErrorиLiteErrorв зависимости от окружения. - Использовать единый интерфейс
ErrorLikeдля всех подсистем.
// Определим две версии базовой ошибки с одним именем:
// + BaseError - Версия для разработки с трассировкой
// + LiteError - Версия для минифицированного приложения
const Base = (mode === 'production' ? LiteError : BaseError) as typeof LiteError
class AppError extends Base { }
// Конкретизированные ошибки
class PermissionError extends AppError {
override readonly name = 'PermissionError'
constructor(message: string) {
super({ message })
}
}
// В режиме 'development', такая ошибка получит поле `stack`
const error = new PermissionError('...')Вариант ошибки без BaseError и управляемым стеком здесь capture.test.ts.
📌 Резервное поле __meta для JSON
__meta - служебный объект, вставляемый только в трех ситуациях:
- Верхний уровень - когда результат не объект (примитив/массив), но нужно вернуть в виде объекта.
- Замещение значения (value placeholder) - когда поле не может быть полноценно сериализовано (
Date,bigint,functionи т.п.). - Усечение - когда массив/объект/уровень был усечен из-за лимитов.
Внутренние свойства __meta однозначно интерпретируют характер контейнера:
type: string- Тип значения (value type). Примеры:'null' | 'boolean' | 'number' | 'string' | 'date' | 'bigint' | 'symbol' | 'regexp' | 'function'. При наличииtype__metaтрактуется как заместитель значения и ожидается полеvalue- представление значения (например ISO дляdate, строка дляbigint, строка-репрезентация дляregexpи т.п.).kind: 'object' | 'array' | 'error'- Тип усеченного или замененного контейнера. Никогда не используется совместно сtype.total: number, truncated: number- Усеченный контейнер(объект или массив) - общее количество элементов и сколько не вошло в результат.length: number- Информация о замещенном контейнере - количество полей объекта или массива.name: string, message?: string- поля для описания ошибок приkind:'error'.
Безопасные методы ошибок(toString(), ...) и функции errorToJsonLike() или errorToString(), позволяют привести любые данные к допустимому типу JSON или строке:
const jsonLike = errorToJsonLike({
he: 'Hello, Error Like!',
get good () { return 'good' },
get bad () { throw new Error('bad') }, // проигнорирует
re: /^[0-9]+$/i,
bg: 123n,
sm: Symbol('hidden'),
dt: new Date('2025-12-25T13:37:00.000Z'),
})Некорректные типы "заворачиваются" в мета-объекты:
{
he: 'Hello, Error Like!',
good: 'good',
re: { __meta: { type: 'regexp', value: '/^[0-9]+$/i' } },
bg: { __meta: { type: 'bigint', value: '123' } },
sm: { __meta: { type: 'symbol', value: 'Symbol(hidden)' } },
dt: { __meta: { type: 'date', value: '2025-12-25T13:37:00.000Z' } }
}Зарезервированное имя поля
__metaизменяется опциейmetaFieldNameи всегда имеет приоритет над именами полей ошибок.
Примеры:
- Объект сериализации верхнего уровня оказался примитивом, массивом или типом который не подлежит структурной сериализации:
{ __meta: { type: 'boolean' | 'number' | ..., value: JsonPrimitive } }
{ __meta: { type: 'array': value: [...] } }- Значение не подлежит структурной сериализации, например
Dateилиbigint:
{ fieldName: { __meta: { type: 'date', value: '2025-10-17T00:13:21.801Z' } } }
{ fieldName: { __meta: { type: 'bigint', value: '541230557920596605488' } } }- Усечение полей объектов или массивов при превышении лимита. Мета-информация добавляется в усеченный контейнер, чтобы сообщить о неполноте данных:
{
fieldName: ...,
__meta: { kind: 'object', total: 75, truncated: 28 }
}
[
item0,
item1,
{ __meta: { kind: 'array', total: 369, truncated: 67 } }
]- Заменитель объекта при превышении глубины:
{
fieldObject: { __meta: { kind: 'object', length: 24 } },
fieldArray: { __meta: { kind: 'array', length: 785 } },
fieldError: { __meta: { kind: 'error', name: 'Error', message?: '...' } }
}⚙️ Требования к окружению и компиляции
js-base-error спроектирован с использованием современных возможностей JavaScript и TypeScript. Для корректной и предсказуемой работы, особенно при наследовании и переопределении полей, проект должен быть настроен с учетом следующих моментов.
Крайне рекомендуется установить в tsconfig.json флаг "useDefineForClassFields": true (или использовать target: "ES2022" и выше, где этот флаг включен по умолчанию):
{
"compilerOptions": {
"useDefineForClassFields": true
}
}Почему это важно?
LiteError, BaseError и abstract ErrorLike определяют стандартные свойства name и message как аксессоры (геттеры/сеттеры) на своем прототипе. Это позволяет синхронизировать значения name и message из detail, что полностью повторяет поведение нативной ошибки.
Когда в классе-наследнике переопределяется:
class MyError extends BaseError {
override readonly name = 'MyError'
}...существует два способа, как TypeScript может это скомпилировать:
- С
"useDefineForClassFields": true(Правильно): Полеnameбудет создано на экземпляре (аналогичноObject.defineProperty). Это "сильная" операция, которая перекрывает аксессор прототипа. Все работает как ожидается - свойство будет успешно собрано вdetail. - С
"useDefineForClassFields": false(Непредсказуемо): Поле класса будет скомпилировано в простое присваивание в конструкторе (this.name = 'MyError'). Эта операция будет перехвачена сеттером прототипа. В результате, свойствоnameне будет создано на экземпляре, а доступ кdetailзапустит механизм преждевременной инициализации(хотя и безопасной).
Результатом обоих вариантов будет один и тот же detail, но преждевременный сбор полей ошибки, которые могут никогда не понадобиться, это не то что мы ожидаем. Для справки: Внутренний механизм нативной ошибки(NodeJS) - так же не форматирует stack, пока нет явного запроса к полю. Chrome меняет только name.
Чтобы гарантировать консистентное и предсказуемое поведение - убедитесь, что используется современный стандарт определения полей классов.
👇 Использование в зависимых библиотеках
Когда несколько библиотек зависят от одного общего пакета js-base-error, рекомендуется указывать его в разделе peerDependencies каждой библиотеки. Это необходимо для обеспечения корректной работы оператора error instanceof ErrorLike. Несогласованность версий или множественные экземпляры js-base-error могут нарушить сравнение через instanceof и логические проверки типов, приводя к неочевидным ошибкам.
Подход, рекомендуемый при работе с библиотеками, разделяющими общие классы:
В библиотеке зависимой от js-base-error peerDependencies:
"peerDependencies": {
"js-base-error": "0.6.0"
}В основном приложении обеспечиваем одну версию js-base-error для всех зависимостей через overrides:
"dependencies": {
"js-base-error": "0.6.0"
},
"overrides": {
"js-base-error": "0.6.0"
}Проверить все установленные версии одного пакета можно командой npm list js-base-error. Если обнаружено несколько версий, команда покажет дерево зависимостей:
[email protected]
├─┬ [email protected]
│ └── [email protected]
└── [email protected]Смотрите так же npm dedupe.

AI-art by ChatGPT
