Last active
February 5, 2025 03:14
-
-
Save ggoodman/492e9bef4c516847f7d9f9e1c6afc7b1 to your computer and use it in GitHub Desktop.
Reusable TypeScript well-known error class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
///<reference types="mocha" /> | |
const assert = require('node:assert/strict'); | |
// Since the source file is TypeScript, we have to require the module itself and | |
// rely on `npm run build` having been called before running tests. ☹️ | |
const { WellKnownError } = require('./well_known_error'); | |
describe(`${WellKnownError.name}`, () => { | |
class TestError extends WellKnownError( | |
{ | |
A: { message: 'A', extra: 'a' }, | |
B: { message: 'B', extra: 'b' }, | |
}, | |
'A' | |
) {} | |
it('will produce errors of the expected shape', () => { | |
const a = new TestError('A'); | |
assert.equal(a.cause, undefined); | |
assert.equal(a.code, 'A'); | |
assert.deepStrictEqual(a.details, { extra: 'a' }); | |
assert.equal(a.message, 'A'); | |
assert.equal(a.name, 'TestError'); | |
assert.ok(a.stack?.startsWith('TestError: A')); | |
}); | |
it('will extract a well-known error from an AbortError', () => { | |
const ac = new AbortController(); | |
ac.abort(new TestError('A')); | |
const a = TestError.extractWellKnownError(ac.signal.reason); | |
assert.ok(TestError.isWellKnownError(a)); | |
assert.equal(a.cause, undefined); | |
assert.equal(a.code, 'A'); | |
assert.deepStrictEqual(a.details, { extra: 'a' }); | |
assert.equal(a.message, 'A'); | |
assert.equal(a.name, 'TestError'); | |
assert.ok(a.stack?.startsWith('TestError: A')); | |
}); | |
it('will wrap a value that is not a well-known error in the default type', () => { | |
const e = new Error('oops'); | |
const a = TestError.wrap(e); | |
assert.ok(TestError.isWellKnownError(a)); | |
assert.equal(a.cause, e); | |
assert.equal(a.code, 'A'); | |
assert.deepStrictEqual(a.details, { extra: 'a' }); | |
assert.equal(a.message, 'A'); | |
assert.equal(a.name, 'TestError'); | |
assert.ok(e.stack); | |
}); | |
it('will will correctly identify well-known fallback errors', () => { | |
const a = new TestError('A'); | |
const b = new TestError('B'); | |
assert.equal(TestError.isFallbackError(a), true); | |
assert.equal(TestError.isFallbackError(b), false); | |
}); | |
it('will throw a TypeError when attempting to construct a well-known error with an invalid code', () => { | |
class TestError extends WellKnownError( | |
{ | |
A: { message: 'A', extra: 'a' }, | |
B: { message: 'B', extra: 'b' }, | |
}, | |
'A' | |
) {} | |
assert.throws(() => { | |
// @ts-expect-error | |
return new TestError('C'); | |
}, TypeError); | |
}); | |
it('will guard against infinite recursion in mutually-referenced errors', () => { | |
const a = new Error(); | |
const b = new Error(); | |
a.cause = b; | |
b.cause = a; | |
assert.equal(TestError.extractWellKnownError(a), undefined); | |
assert.equal(TestError.extractWellKnownError(b), undefined); | |
}); | |
it('will throw a well-known error given as the cause for an AbortController abort', () => { | |
const ac = new AbortController(); | |
const reason = new TestError('A'); | |
ac.abort(reason); | |
assert.throws(() => TestError.throwIfAborted(ac.signal, 'B'), { | |
name: TestError.name, | |
message: 'A', | |
cause: undefined, | |
}); | |
}); | |
it('will throw a well-known error of the supplied type when an AbortSignal is aborted with an explicit reason that is not a well-known error', () => { | |
const ac = new AbortController(); | |
const reason = new Error('oops'); | |
ac.abort(reason); | |
assert.throws(() => TestError.throwIfAborted(ac.signal, 'B'), { | |
name: TestError.name, | |
message: 'B', | |
cause: reason, | |
}); | |
}); | |
it('will throw a well-known error of the supplied type when an AbortSignal is aborted with an implicit reason that is not a well-known error', () => { | |
const ac = new AbortController(); | |
ac.abort(); | |
assert.throws(() => TestError.throwIfAborted(ac.signal, 'B'), { | |
name: TestError.name, | |
message: 'B', | |
// We refer to the source of truth because it's difficult to recreate this synthetically | |
cause: ac.signal.reason, | |
}); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The `WellKnownError` class is a base class for well-known error types. It | |
* provides a way to define a set of well-known error types and create instances | |
* of those errors with a given error code and optional error details. | |
* | |
* In addition, it provides easy ways to extract well known errors from the | |
* causal chain of an error. | |
* | |
* @example | |
* ```ts | |
* class ServerError extends WellKnownError( | |
* { | |
* BAD_REQUEST_PAYLOAD: { | |
* message: "Invalid request payload", | |
* statusCode: 400, | |
* }, | |
* ENDPOINT_NOT_FOUND: { | |
* message: "Endpoint not found", | |
* statusCode: 404, | |
* }, | |
* SERVER_ERROR: { | |
* message: "An unknown error occurred", | |
* statusCode: 500, | |
* }, | |
* }, | |
* 'SERVER_ERROR' | |
* ) {} | |
* | |
* const validationError = validate(payload, schema); | |
* if (validationError != null) { | |
* throw new ServerError("BAD_REQUEST_PAYLOAD", { cause: validationError }); | |
* } | |
* ``` | |
* | |
* @module std/well_known_error | |
*/ | |
type StringKeys<T> = Extract<keyof T, string>; | |
export interface WellKnownErrorDetails { | |
message: string; | |
} | |
// Back-fill error cause for earlier JavaScript versions | |
declare global { | |
interface ErrorOptions { | |
cause?: unknown; | |
} | |
interface ErrorConstructor { | |
new (message?: string, options?: ErrorOptions): Error; | |
} | |
interface Error { | |
cause?: unknown; | |
} | |
} | |
export interface WellIKnownErrorConstructor< | |
TErrorTypes extends Record<string, WellKnownErrorDetails>, | |
TFallbackErrorCode extends StringKeys<TErrorTypes>, | |
> { | |
/** | |
* Creates a new `WellKnownError` instance with the given error code and | |
* optional error details. | |
*/ | |
new ( | |
errorCode: StringKeys<TErrorTypes>, | |
details?: ErrorOptions | |
): WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]>; | |
readonly prototype: WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[keyof TErrorTypes]>; | |
/** | |
* Extract a well-known error from an unknown value by considering the value itself and its causal chain. | |
* | |
* @param e an unknown value that might be a well-known error itself or have one in its causal chain | |
*/ | |
extractWellKnownError( | |
e: unknown | |
): WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]> | undefined; | |
/** | |
* Returns true if the given value is a `WellKnownError` instance with the | |
* fallback error code. | |
* | |
* @param e the value to test | |
*/ | |
isFallbackError( | |
e: unknown | |
): e is WellKnownError<TFallbackErrorCode, TErrorTypes[TFallbackErrorCode]>; | |
/** | |
* Returns true if the given value is a `WellKnownError` instance. | |
* | |
* @param e the value to test | |
*/ | |
isWellKnownError( | |
e: unknown | |
): e is WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]>; | |
/** | |
* Check if the supplied `AbortSignal` is aborted and if so, throws a `wrap`ped version | |
* of the `.reason`. | |
* | |
* @param signal an abort signal to test | |
*/ | |
throwIfAborted(signal: AbortSignal, errorCode: StringKeys<TErrorTypes>): void; | |
/** | |
* Wrap an unknown value as the default `WellKnownError` type or return the value | |
* if it is already a `WellKnownError`. | |
* | |
* @param e a potential well-known error or a value to wrap as the default error code | |
*/ | |
wrap(e: unknown): WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]>; | |
} | |
export interface WellKnownError< | |
TErrorCode extends string, | |
TErrorDetails extends WellKnownErrorDetails, | |
> extends Error { | |
readonly code: TErrorCode; | |
readonly message: TErrorDetails['message']; | |
readonly details: Omit<TErrorDetails, 'message'>; | |
} | |
/** | |
* Create a WellKnownError class with a set of well-known error codes and a fallback error code | |
* | |
* @param errorTypes A mapping of error codes to structured data about errors of those types | |
* @param fallbackErrorCode The default error code to use when wrapping unknown values | |
* @returns A WellKnownError base class suitable for extending | |
*/ | |
export function WellKnownError< | |
const TErrorTypes extends Record<string, WellKnownErrorDetails>, | |
TFallbackErrorCode extends StringKeys<TErrorTypes>, | |
>( | |
errorTypes: TErrorTypes, | |
fallbackErrorCode: TFallbackErrorCode | |
): WellIKnownErrorConstructor<TErrorTypes, TFallbackErrorCode> { | |
class WellKnownErrorImpl<TErrorCode extends StringKeys<TErrorTypes>> extends Error { | |
static extractWellKnownError( | |
e: unknown | |
): WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]> | undefined { | |
const seen = new Set(); | |
while (e != null && typeof e === 'object') { | |
if (WellKnownErrorImpl.isWellKnownError(e)) { | |
return e; | |
} | |
if (seen.has(e)) { | |
// Safe-guard against wacky situations where there are circular references | |
// in `.cause` chains. Highly unlikely but probably worth guarding against | |
// since the alternative is infinite recursion and crashes. | |
return undefined; | |
} | |
seen.add(e); | |
if (!('cause' in e)) { | |
return; | |
} | |
e = e['cause']; | |
} | |
return undefined; | |
} | |
static isFallbackError( | |
e: unknown | |
): e is WellKnownError<TFallbackErrorCode, TErrorTypes[TFallbackErrorCode]> { | |
return WellKnownErrorImpl.isWellKnownError(e) && e.code === fallbackErrorCode; | |
} | |
static isWellKnownError( | |
e: unknown | |
): e is WellKnownError<StringKeys<TErrorTypes>, TErrorTypes[StringKeys<TErrorTypes>]> { | |
return e instanceof WellKnownErrorImpl; | |
} | |
static throwIfAborted(signal: AbortSignal, code: StringKeys<TErrorTypes>): void { | |
if (!signal.aborted) { | |
return; | |
} | |
if (this.isWellKnownError(signal.reason)) { | |
throw signal.reason; | |
} | |
const err = new this(code, { cause: signal.reason }); | |
Error.captureStackTrace?.(err, this.throwIfAborted); | |
throw err; | |
} | |
static wrap(e: unknown): WellKnownErrorImpl<StringKeys<TErrorTypes>> { | |
if (WellKnownErrorImpl.isWellKnownError(e)) { | |
return e; | |
} | |
const err = new this(fallbackErrorCode, { cause: e }); | |
Error.captureStackTrace(err, this.wrap); | |
return err; | |
} | |
readonly code: TErrorCode; | |
readonly details: Omit<TErrorTypes[TErrorCode], 'message'>; | |
readonly message: TErrorTypes[TErrorCode]['message']; | |
constructor(errorCode: TErrorCode, options?: ErrorOptions) { | |
const errorDetails = errorTypes[errorCode]; | |
if (typeof errorCode !== 'string') { | |
throw new TypeError('Expected errorCode to be a string'); | |
} | |
if (errorDetails == null) { | |
throw new TypeError(`Unknown error code: ${JSON.stringify(errorCode)}`); | |
} | |
super(errorDetails.message, options); | |
const { message, ...details } = errorDetails; | |
this.code = errorCode; | |
this.cause = options?.cause; | |
this.details = details; | |
this.message = message; | |
this.name = new.target.name; | |
} | |
} | |
return WellKnownErrorImpl; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment