Skip to content

Commit

Permalink
feat(options): expose the .cause native error option
Browse files Browse the repository at this point in the history
  • Loading branch information
uladkasach committed Jun 18, 2024
1 parent 106fae2 commit cadbf3b
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 8 deletions.
23 changes: 23 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,26 @@ const phone = customer.phoneNumber ?? UnexpectedCodePathError.throw(
{ customer },
);
```

### HelpfulError parameter options.cause

The .cause parameter is a helpful feature of native errors. It allows you to chain errors together in a way that retains the full stack trace across errors.

For example, sometimes, the original error that your code experiences can be reworded to make it easier to debug. By using the .cause option, you're able to retain the stack trace and reference of the original error while throwing a new, more helpful, error.

```ts
// imagine you're using some api which throws an unhelpful error
const apiGetS3Object = async (input: { key: string }) => { throw new Error("no access") }

// you can catch and extend the error to add more context
const helpfulGetS3Object = async (input: { key: string }) => {
try {
await getS3Object();
} catch (error) {
if (error.message === "no access") throw HelpfulError("getS3Object.error: could not get object", {
cause: error, // !: by adding the "cause" here, we'll retain the stack trace of the original error
input,
})
}
}
```
4 changes: 2 additions & 2 deletions src/BadRequestError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HelpfulError } from './HelpfulError';
import { HelpfulError, HelpfulErrorMetadata } from './HelpfulError';

/**
* BadRequestError errors are used to explicitly declare that your logic has successfully rejected a request
Expand All @@ -11,7 +11,7 @@ import { HelpfulError } from './HelpfulError';
* - e.g., the [simple-lambda-handlers](https://github.com/ehmpathy/simple-lambda-handlers) library returns an error to the caller (to notify them of the rejection) while marking the lambda invocation as successful (to avoid cloudwatch metric errors and automated retries)
*/
export class BadRequestError extends HelpfulError {
constructor(message: string, metadata?: Record<string, any>) {
constructor(message: string, metadata?: HelpfulErrorMetadata) {
super(['BadRequestError: ', message].join(''), metadata);
}
}
9 changes: 9 additions & 0 deletions src/HelpfulError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ describe('HelpfulError', () => {
expect(error).toBeInstanceOf(HelpfulError);
expect(error.message).toEqual('phone two not found!');
});
it('should be possible to extend the call stack of an error via cause', () => {
const errorOriginal = new Error('some original error');
const errorHelpful = new HelpfulError('some helpful error', {
cause: errorOriginal,
});
expect(errorHelpful).toMatchSnapshot();
expect(errorHelpful.cause).toBeDefined();
expect(errorHelpful.cause).toMatchSnapshot();
});
});
17 changes: 13 additions & 4 deletions src/HelpfulError.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { omit } from 'type-fns';

export type HelpfulErrorMetadata = Record<string, any> & { cause?: Error };

/**
* HelpfulError errors are used to add information that helps the future observer of the error understand whats going on
*/
export class HelpfulError extends Error {
constructor(message: string, metadata?: Record<string, any>) {
constructor(message: string, metadata?: HelpfulErrorMetadata) {
const metadataWithoutCause = metadata
? omit(metadata, ['cause'])
: metadata;
const fullMessage = `${message}${
metadata ? `\n\n${JSON.stringify(metadata)}` : ''
metadataWithoutCause && Object.keys(metadataWithoutCause).length
? `\n\n${JSON.stringify(metadataWithoutCause)}`
: ''
}`;
super(fullMessage);
super(fullMessage, metadata?.cause ? { cause: metadata.cause } : undefined);
}

/**
Expand All @@ -20,7 +29,7 @@ export class HelpfulError extends Error {
public static throw<T extends typeof HelpfulError>(
this: T, // https://stackoverflow.com/a/51749145/3068233
message: string,
metadata?: Record<string, any>,
metadata?: HelpfulErrorMetadata,
): never {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw new this(message, metadata) as InstanceType<T>;
Expand Down
4 changes: 2 additions & 2 deletions src/UnexpectedCodePathError.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HelpfulError } from './HelpfulError';
import { HelpfulError, HelpfulErrorMetadata } from './HelpfulError';

/**
* UnexpectedCodePathError errors are used to explicitly declare that we've reached a code path that should never have been reached
*/
export class UnexpectedCodePathError extends HelpfulError {
constructor(message: string, metadata?: Record<string, any>) {
constructor(message: string, metadata?: HelpfulErrorMetadata) {
super(['UnexpectedCodePathError: ', message].join(''), metadata);
}
}
4 changes: 4 additions & 0 deletions src/__snapshots__/HelpfulError.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`HelpfulError should be possible to extend the call stack of an error via cause 1`] = `[Error: some helpful error]`;

exports[`HelpfulError should be possible to extend the call stack of an error via cause 2`] = `[Error: some original error]`;

exports[`HelpfulError should produce a helpful, observable error message 1`] = `
[Error: the dogs were let out
Expand Down

0 comments on commit cadbf3b

Please sign in to comment.