export type StandardError< TDetails extends Record = any, TCause extends StandardError = any, > = { code: string message?: string retry?: boolean | number stack?: string cause?: TCause details?: TDetails } export class AppError< TDetails extends Record = Record, TCause extends StandardError = StandardError, > extends Error implements StandardError { code: string override message: string override stack?: string override cause?: TCause details?: TDetails retry?: boolean | number constructor({ code, message, stack, details, cause, retry }: StandardError) { super( AppError.renderString({ code: code || 'Error', message: (message || stack) as string, stack, details, cause, retry, }), ) this.code = code this.message = message as string this.stack = stack || cause?.stack this.details = details this.cause = cause this.retry = retry } static causedBy< TDetails extends Record = Record, TCause extends StandardError = StandardError, >( cause: Error | StandardError, err?: StandardError, ): AppError { if (err) { const message = cause.message || JSON.stringify(cause) return new AppError({ ...err, cause: { ...cause, code: (cause as any)?.code || (cause as any)?.errno || (cause as any)?.type || (cause as any)?.name || 'Error', message: message !== '{}' ? message : cause?.toString() || cause?.stack, }, }) } else { const message = cause.message || JSON.stringify(cause) return new AppError({ ...cause, code: (cause as any)?.code || (cause as any)?.errno || (cause as any)?.type || (cause as any)?.name || 'Error', message: message !== '{}' ? message : cause?.toString() || JSON.stringify(cause), stack: cause.stack, }) } } override toString(): string { return AppError.renderString(this) } toJSON(): StandardError { return { code: this.code, message: this.message, stack: this.stack, cause: this.cause, details: this.details, retry: this.retry, } } static renderString(appError: StandardError): string { const obj = appError let fullMessage = `${obj.code}: ${obj.message}${ obj.details ? ` => ${JSON.stringify(obj.details)}` : '' }` if (typeof obj.cause === 'object') { if (obj.cause instanceof AppError) { fullMessage += `${fullMessage}\n -> caused by ${obj.cause.toString()}` } else { fullMessage += `${fullMessage}\n -> caused by ${new AppError({ ...obj.cause, code: (obj.cause as any)?.code || 'Error', }).toString()}` } } if (obj.stack || obj.cause?.stack) { fullMessage += `\n${obj.stack ?? obj.cause?.stack}` } return fullMessage } }