Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Last active April 14, 2023 16:44
Show Gist options
  • Save zerkalica/f2507ddafcc7277e3be435dcc80082ff to your computer and use it in GitHub Desktop.
Save zerkalica/f2507ddafcc7277e3be435dcc80082ff to your computer and use it in GitHub Desktop.
errors.ts
declare class AggregateError extends Error {
readonly errors: (Error | object | string)[];
constructor(errors: readonly (Error | object | string)[], message: string, options?: { cause: unknown });
}
function createAggregateErrorStub(): typeof AggregateError {
return class AggregateErrorStub extends Error {
constructor(readonly errors: (Error | object | string)[], message: string, options?: { cause: unknown }) {
// @ts-ignore
super(message, options);
this.name = 'AggregateError';
this.errors = errors;
}
};
}
export const AggregateErrorSafe = typeof AggregateError === 'undefined' ? createAggregateErrorStub() : AggregateError;
export type CauseOptBase = {
__typename?: string | undefined;
label?: string | null;
message?: string | null;
code?: string | null;
statusCode?: string;
};
function isPresent<T>(t: T | undefined | null | void | false): t is T {
return t !== undefined && t !== null && t !== false;
}
type ErrorEx<CauseOpt> = {
code?: string | null;
statusCode?: string | null;
cause: CauseOpt;
};
/**
* Библиотека нормализации ошибок, построенная на исключениях.
* Пытается оставлять первоначальный инстанс ошибки, дописывая контекст в cause по мере всплытия.
* Множество ошибок запаковываются в AggregateError.
* Также может извлекать Union ошибок из Error.cause
*
@example
```ts
type ErrorDto = {
__typename: 'a',
a: string
} | {
__typename: 'b'
b: number;
}
class MyError extends ErrorBase<ErrorDto> {}
try {
const {data, errors} = gqlFetch() as {data: unknown, errors: ErrorDto[]}
if (errors.length || ! data) {
throw MyError.ensure([
...errors,
! data && {
__typename: 'a',
a: '123'
}
])
}
} catch (e) {
const causes = MyError.causes(e)
causes.map(cause => cause.__typename === 'a' && cause.a)
}
```
*/
export class ErrorBase<CauseOpt extends CauseOptBase = CauseOptBase> extends Error {
cause = {} as CauseOpt;
static make<Instance>(this: { new (): Instance }, config: Partial<Instance> = {}): Instance {
return Object.assign(new this(), config);
}
static ensure<Instance extends { cause: CauseOptBase }>(
this: { new (): Instance },
orig: Error | readonly Error[] | Instance['cause'] | readonly (Instance['cause'] | null | undefined | false)[],
cause?: Instance['cause'],
skip = 0,
) {
const obj = new this() as Instance & ErrorBase<Instance['cause']>;
return obj.ensure(orig, cause, skip + 1);
}
static causes<Instance extends { cause: CauseOptBase }>(this: { new (): Instance }, some: unknown, skip = 0) {
const obj = new this() as Instance & ErrorBase<Instance['cause']>;
return obj.causes(some, skip + 1);
}
// Что б дважды не нормализовывать один и тот же объект.
protected static normalized = new WeakMap<object, Error>();
ensure(
orig: Error | readonly Error[] | CauseOpt | readonly (CauseOpt | null | undefined | false)[],
cause?: CauseOpt,
skip = 0,
) {
return this.normalize(orig, cause, skip + 1);
}
normalize(
orig: Error | readonly Error[] | CauseOpt | readonly (CauseOpt | null | undefined | false)[] | unknown,
cause?: CauseOpt,
// При нормализации могут создаваться новые инстансы Error, из стека вырезаются трейсы внутри либы, что б не замусоривать
skip = 0,
) {
// Один и тот же объект может быть брошен как ошибка и пропущен через ensure несколько раз.
// Стараемся получить первоначальный нормализованный инстанс ошибки, используя объект как ключ в WeakMap.
// Для примитивных типов, WeakMap не работает, обычный Map приведет к утечке памяти.
// Поэтому, если orig - примитивный тип, будет создаваться каждый раз новый инстанс Error.
const key = typeof orig === 'object' || (typeof orig === 'function' && orig) ? orig : undefined;
const normalized = key ? ErrorBase.normalized.get(key) : undefined;
let err: undefined | (Error & Partial<ErrorEx<CauseOpt>>) =
normalized ?? (orig instanceof Error ? orig : undefined);
if (!err) {
// Всем, далее создаваемым ошибкам назначаем стек, вырезая из него мусорные трейсы
const stack = this.stackFilter(new Error().stack, skip + 1);
const errors = (Array.isArray(orig) ? orig : [orig])
.map((some: (Error & Partial<ErrorEx<CauseOpt>>) | CauseOpt | string | number | null | undefined) =>
typeof some === 'object' && some !== null ? some : ({ message: String(some ?? 'UNK') } as CauseOpt),
)
.map((some) => {
if (some instanceof Error) return some;
try {
return Object.assign(new Error(some.message ?? some.code ?? some.__typename ?? 'UNK'), {
stack,
cause: some ?? undefined,
code: some.code ?? undefined,
statusCode: some.statusCode ?? undefined,
});
} catch (e) {
return new Error('Error building error from DTO');
}
});
if (errors.length === 0) {
err = Object.assign(new Error('Empty array in ErrorBase.ensure'), { stack });
} else if (errors.length === 1) {
err = errors[0];
} else {
let messages = [] as string[];
try {
messages = errors.map((err) => err.message ?? 'UNK');
} catch (e) {
messages = [`Can't build message for AggregateError`];
}
err = Object.assign(new AggregateErrorSafe(errors, `[${errors.length}] ${messages.join(', ')}`), {
stack,
});
}
if (key) ErrorBase.normalized.set(key, err);
}
try {
// Падать при нормализации ошибок никак нельзя.
// В orig может прийти прокся, которая бросает ексепшен, при обращении к свойствам.
if (!err.cause || typeof err.cause !== 'object') err.cause = {} as CauseOpt;
if (cause) {
// Мержим cause в err.cause
// Не перезатираем существующие значения в err.cause, добавляем с суффиксом
for (const prop of Object.keys(cause)) {
const prev = err.cause[prop as keyof CauseOpt];
const next = cause[prop as keyof CauseOpt];
err.cause[prop as keyof CauseOpt] = next;
if (!prev || prev === next) continue;
for (let i = 1; i < 99; i++) {
const key = `${prop}-${i}` as keyof CauseOpt;
if (err.cause[key]) continue;
err.cause[key] = prev;
break;
}
}
}
if (cause?.message && !err.message.includes(cause.message)) {
// Когда передаем ошибку в ensure вместе с каузой и в каузе есть message,
// дописываем его к основному message ошибки,
// предварительно проверив, что оно уже не дописано ранее.
err.message = `${err.message ? `${err.message}: ` : ''}${cause.message}`;
}
if (!err.message) err.message = 'UNKNOWN';
if (!err.stack?.match(/^\s+at\s+.*/)) {
// Некоторые кривые ошибки из fs приходят с пустым stack.
// Пытаемся восстановить стек, пускай от вызова ensure.
// Все внешние апи стоит оборачивать в try { unstable() } catch(e) { throw ErrorBase.ensure(e. { some context }) }
err.stack = this.stackFilter(new Error().stack, skip + 1);
}
// code - часто используется как поле в ошибке, дублируем его из каузы, если нет в ошибке
if (err.code === undefined) err.code = err.cause?.code ?? undefined;
// http statusCode более-менее стандартный в некоторых node-фреймворках,
// его тоже прикидываем из каузы, если не заполнен
if (err.statusCode === undefined) err.statusCode = err.cause?.statusCode ?? undefined;
// в label - имя функции или метода, из которого был вызван ensure первый раз для этой ошибки
if (!err.cause.label) err.cause.label = this.funcFromStack(err.stack);
} catch (e) {
console.warn(e);
}
return err as Error & ErrorEx<CauseOpt>;
}
causes(some: unknown, skip = 0) {
const host = this.normalize(some, undefined, skip + 1);
const errors = this.unpack(host);
return errors.map((error) => error.cause).filter(isPresent);
}
protected unpack<Err extends Error>(some: Err, result = [] as Err[]) {
if (some instanceof AggregateErrorSafe) {
for (const err of some.errors) {
if (err instanceof Error) this.unpack(err, result);
}
} else {
result.push(some);
}
return result;
}
protected funcFromLine(line?: string) {
return line?.match(/^\s+at\s+(?:(?:async\s+)?([^\s]*))/)?.[1];
}
protected funcFromStack(stack?: string) {
const parts = stack?.split('\n') ?? [];
return this.funcFromLine(parts[1]);
}
protected stackFilter(stack?: string, skip = 1) {
const parts = stack?.split('\n') ?? [];
return [parts[0], ...parts.slice(skip + 1)].join('\n');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment