UNPKG

errlop

Version:

An extended Error class that envelops a parent error, such that the stack trace contains the causation

207 lines (178 loc) 5.9 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /** A valid Errlop or Error instance. */ export type ErrorValid = Errlop | Error /** Properties {@link Errlop} supports. */ export interface ErrorProperties { /** The description of this error. */ message: string /** The code to identify this error. If a string code, it will be included in the stack. */ code: string | number /** The severity level of this error. */ level: string | number /** The parent that caused this error, if any. If not provided, inherited from `cause` property. */ parent: ErrorValid | null /** An array of the {@link parent} lineage. Most immediate to most distant. */ ancestors: Array<ErrorValid> /** A numeric code that can be used for the process exit status. If not provided, inherited from {@link ancestors} `exitCode`, `errno`, `code` numeric properties. */ exitCode: number | null /** The stack of this error alone, without any ancestry. */ orphanStack: string /** The stack of this error including its ancestry. */ stack: string } /** An input that can become an Errlop or Error instance. */ export type ErrorInput = | Errlop | Error | Partial<ErrorProperties> | string | any /** * Convert to a valid number or null. * @param input */ function getNumber(input: any): number | null { if (input == null || input === '') return null const number = Number(input) if (isNaN(number)) return null return number } /** * Fetch the exit code from the value * @param value */ function getExitCode(value: any): number | null { if (value != null) { if (typeof value.exitCode !== 'undefined') return getNumber(value.exitCode) if (typeof value.errno !== 'undefined') return getNumber(value.errno) if (typeof value.code !== 'undefined') return getNumber(value.code) } return null } /** * Prepend a code to the stack if applicable. * @param code * @param stack */ function prependCode(code: any, stack: string): string { if (code && typeof code === 'string' && stack.includes(code) === false) return `[${code}]: ${stack}` return stack } /** Errlop, an extended Error class that envelops a parent Error to provide ancestry stack inforation. */ export default class Errlop extends Error implements ErrorProperties { // implements parent: ErrorValid | null = null ancestors: Array<ErrorValid> = [] exitCode: number | null = null orphanStack: string = '' declare stack: string // declare to inherit from super declare message: string // declare to inherit form super code: string | number = '' level: string | number = '' /** Duck typing so native classes can work with transpiled classes, as otherwise they would fail instanceof checks. */ klass: typeof Errlop = Errlop /** * Turn the input and parent into an Errlop instance. * @param input * @param parent */ constructor(input: ErrorInput, parent: ErrorInput = null) { if (!input) throw new Error('Attempted to create an Errlop without an input') // construct with message super(input.message || input) // parent // if override not set, fallback to parent or cause if (!parent) parent = input.parent || input.cause // if override, or parent/cause was set, then set this.parent if (parent) { if (Errlop.isError(parent)) { this.parent = parent } else { this.parent = new Errlop(parent) } } // ancestors, assumed to only exist on Errlop if (this.parent) { this.ancestors.push(this.parent) if (Errlop.isErrlop(this.parent)) { this.ancestors.push(...this.parent.ancestors) } } // exitCode, code, level for (const error of [input, this, ...this.ancestors]) { if (this.exitCode == null) { this.exitCode = getExitCode(error) } if (this.code === '' && error.code != null && error.code !== '') { this.code = getNumber(error.code) ?? error.code.toString() } if (this.level === '' && error.level != null && error.level !== '') { this.level = getNumber(error.level) ?? error.level.toString() } } // orphanStack this.orphanStack = prependCode( this.code, ( input.orphanStack || input.stack || this.stack || // this.stack should exist, unless something that extended Errlop broke it this.message || this || '' ).toString() ) // stack this.stack = [this, ...this.ancestors] .map(function (error) { // error is either Errlop or Error, however that doesn't stop extenders from breaking them return prependCode( (error as any).code, ( (error as Errlop).orphanStack || error.stack || // those should exist, unless something that extended Error broke it error.message || error || '' ).toString() ) }) .filter((s) => Boolean(s)) // filter out Error instances that have no stack .join(Errlop.stackSeparator) } // static methods /** The separator to use for the stack entries */ static stackSeparator = '\n↳ ' /** Check whether or not the value is an Errlop instance */ static isErrlop(value: Errlop): true static isErrlop(value?: any): value is Errlop static isErrlop(value?: any): boolean { return value && (value instanceof this || value.klass === this) } /** Check whether or not the value is an Errlop or Error instance. */ static isError(value: ErrorValid): true static isError(value?: any): value is ErrorValid static isError(value?: any): boolean { return value instanceof Error || Errlop.isErrlop(value) } /** * Ensure that the value is an Errlop instance * @param value */ static ensure(value: ErrorInput): Errlop { return this.isErrlop(value) ? value : this.create(value, null) } /** * Syntactic sugar for Errlop class creation. * Enables `Errlop.create(...)` to achieve `new Errlop(...)` * @param input * @param parent */ static create(input: ErrorInput, parent: ErrorInput = null): Errlop { return new this(input, parent) } }