From 3f82ef65c0a53d08b7cddf26b5fd6ef1f7079fba Mon Sep 17 00:00:00 2001 From: Ulad Kasach Date: Sun, 16 Jun 2024 08:36:24 -0400 Subject: [PATCH] feat(factory): add static .throw factory to helpful errors for convinience --- readme.md | 21 +++++++++++++++++++++ src/BadRequestError.test.ts | 20 ++++++++++++++++++++ src/HelpfulError.test.ts | 22 +++++++++++++++++++++- src/HelpfulError.ts | 17 +++++++++++++++++ src/UnexpectedCodePathError.test.ts | 22 ++++++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 4c44be8..00a32eb 100644 --- a/readme.md +++ b/readme.md @@ -138,3 +138,24 @@ expect(error).toBeInstanceOf(HelpfulError); expect(error.message).toContain('found me') ``` +### .throw + +The errors extended from the `HelpfulError` include a `.throw` static method for convenient usage with ternaries or condition chains + +For example, instead of +```ts +const phone = customer.phoneNumber ?? (() => { + throw new UnexpectedCodePathError( + 'customer has relationship without phone number. how is that possible?', + { customer }, + ); +})(); +``` + +You can simply write +```ts +const phone = customer.phoneNumber ?? UnexpectedCodePathError.throw( + 'customer does not have a phone. how is that possible?', + { customer }, +); +``` diff --git a/src/BadRequestError.test.ts b/src/BadRequestError.test.ts index 5de6890..0137cbf 100644 --- a/src/BadRequestError.test.ts +++ b/src/BadRequestError.test.ts @@ -1,4 +1,5 @@ import { BadRequestError } from './BadRequestError'; +import { getError } from './getError'; describe('BadRequestError', () => { it('should produce a helpful, observable error message', () => { @@ -7,4 +8,23 @@ describe('BadRequestError', () => { }); expect(error).toMatchSnapshot(); }); + it('should be throwable in a ternary conveniently and precisely', () => { + const error = getError(() => { + // this case should not throw + const customerOne: { phone: string | null } = { + phone: 'yes', + }; + const phoneOne = + customerOne.phone ?? BadRequestError.throw('phone one not found!'); + + // but this case should throw + const customerTwo: { phone: string | null } = { + phone: null, + }; + const phoneTwo = + customerTwo.phone ?? BadRequestError.throw('phone two not found!'); + }); + expect(error).toBeInstanceOf(BadRequestError); + expect(error.message).toContain('phone two not found!'); + }); }); diff --git a/src/HelpfulError.test.ts b/src/HelpfulError.test.ts index 5fca737..9ffd2fa 100644 --- a/src/HelpfulError.test.ts +++ b/src/HelpfulError.test.ts @@ -1,10 +1,30 @@ import { HelpfulError } from './HelpfulError'; +import { getError } from './getError'; describe('HelpfulError', () => { it('should produce a helpful, observable error message', () => { const error = new HelpfulError('the dogs were let out', { - who: 'your mom', + who: 'your mom', // 🙄😂 }); expect(error).toMatchSnapshot(); }); + it('should be throwable in a ternary conveniently and precisely', () => { + const error = getError(() => { + // this case should not throw + const customerOne: { phone: string | null } = { + phone: 'yes', + }; + const phoneOne = + customerOne.phone ?? HelpfulError.throw('phone one not found!'); + + // but this case should throw + const customerTwo: { phone: string | null } = { + phone: null, + }; + const phoneTwo = + customerTwo.phone ?? HelpfulError.throw('phone two not found!'); + }); + expect(error).toBeInstanceOf(HelpfulError); + expect(error.message).toEqual('phone two not found!'); + }); }); diff --git a/src/HelpfulError.ts b/src/HelpfulError.ts index 11afbef..6a83c80 100644 --- a/src/HelpfulError.ts +++ b/src/HelpfulError.ts @@ -8,4 +8,21 @@ export class HelpfulError extends Error { }`; super(fullMessage); } + + /** + * a utility to throw an error of this class, for convenience + * + * e.g., + * ```ts + * const phone = customer.phone ?? HelpfulError.throw('expected a phone'); + * ``` + */ + public static throw( + this: T, // https://stackoverflow.com/a/51749145/3068233 + message: string, + metadata?: Record, + ): never { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw new this(message, metadata) as InstanceType; + } } diff --git a/src/UnexpectedCodePathError.test.ts b/src/UnexpectedCodePathError.test.ts index 50a43c3..737e09f 100644 --- a/src/UnexpectedCodePathError.test.ts +++ b/src/UnexpectedCodePathError.test.ts @@ -1,4 +1,5 @@ import { UnexpectedCodePathError } from './UnexpectedCodePathError'; +import { getError } from './getError'; describe('UnexpectedCodePathError', () => { it('should produce a helpful, observable error message', () => { @@ -7,4 +8,25 @@ describe('UnexpectedCodePathError', () => { }); expect(error).toMatchSnapshot(); }); + it('should be throwable in a ternary conveniently and precisely', () => { + const error = getError(() => { + // this case should not throw + const customerOne: { phone: string | null } = { + phone: 'yes', + }; + const phoneOne = + customerOne.phone ?? + UnexpectedCodePathError.throw('phone one not found!'); + + // but this case should throw + const customerTwo: { phone: string | null } = { + phone: null, + }; + const phoneTwo = + customerTwo.phone ?? + UnexpectedCodePathError.throw('phone two not found!'); + }); + expect(error).toBeInstanceOf(UnexpectedCodePathError); + expect(error.message).toContain('phone two not found!'); + }); });