const isString = (value: unknown): value is string => { return typeof value === "string" } const isObject = (value: unknown): value is object => { return typeof value === "object" && value !== null } const extractMessage = (data: unknown): string | undefined => { if (isString(data)) { return data } else if (isObject(data) && "message" in data && isString(data.message)) { return data.message } else { return undefined } } const extractCause = (data: unknown): unknown => { if (isObject(data) && "cause" in data) { return data.cause } else { return undefined } } const formatCauseStack = (cause: unknown): string => { if (cause instanceof Error === false || cause.stack === undefined) { return "" } const indented = cause.stack.replaceAll("\n", "\n ") return `\nCaused by: ${indented}` } const serializeCause = (cause: unknown): unknown => { if (cause instanceof Error) { return { name: cause.name, message: cause.message, stack: cause.stack } } return cause } export interface SerializedTaggedError { version: "1" // for future compatibility tag: Tag data: Data name: string message: string cause: unknown | undefined stack: string | undefined timestamp: number } export interface TaggedErrorInstance extends Error { readonly tag: Tag readonly data: Data toJSON(): SerializedTaggedError } export interface TaggedErrorClass { new (data: Data): TaggedErrorInstance is(target: unknown): target is TaggedErrorInstance } export const createTaggedErrorClass = (tag: Tag) => { return (): TaggedErrorClass => { return class ImplementError extends Error { readonly tag: Tag readonly data: Data constructor(data: Data) { const message = extractMessage(data) const cause = extractCause(data) super(message, cause !== undefined ? { cause } : undefined) Object.setPrototypeOf(this, new.target.prototype) // oxlint-disable-next-line unicorn/custom-error-definition this.name = tag this.stack = `${this.stack}${formatCauseStack(cause)}` this.tag = tag this.data = data } static is(target: unknown): target is TaggedErrorInstance { if (target instanceof ImplementError) { return true } if (isTaggedError(target) && target.tag === tag) { return true } return false } toJSON(): SerializedTaggedError { return { version: "1", tag: this.tag, data: this.data, name: this.name, message: this.message, cause: serializeCause(this.cause), stack: this.stack, timestamp: Date.now(), } } } } } export const isTaggedError = ( target: unknown, ): target is TaggedErrorInstance => { if (typeof target !== "object" || target === null) { return false } if (target instanceof Error === false) { return false } if (!("tag" in target) || typeof target.tag !== "string") { return false } if (!("data" in target)) { return false } if (!("toJSON" in target) || typeof target.toJSON !== "function") { return false } return true } export type AnyTaggedError = TaggedErrorInstance