Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Last active February 5, 2025 03:14
Show Gist options
  • Save ggoodman/492e9bef4c516847f7d9f9e1c6afc7b1 to your computer and use it in GitHub Desktop.
Save ggoodman/492e9bef4c516847f7d9f9e1c6afc7b1 to your computer and use it in GitHub Desktop.
Reusable TypeScript well-known error class
// @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,
});
});
});
/**
* 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