diff --git a/.changeset/real-mice-beg.md b/.changeset/real-mice-beg.md index bcb4f0ca9a..7dde97f6c2 100644 --- a/.changeset/real-mice-beg.md +++ b/.changeset/real-mice-beg.md @@ -3,3 +3,5 @@ --- Two new helpers have been added that make it easier to migrate to considering thrown errors to be of the type `unknown` in TypeScript. The helpers are `assertError` and `isError`, and can be called to make sure that an unknown value conforms to the shape of an `ErrorLike` object. The `assertError` function is a type-guard that throws in the case of a mismatch, while `isError` returns false. + +A new error constructor has also been added, `ForwardedError`, which can be used to add context to a forwarded error. It requires both a message and a cause, and inherits the `name` property from the `cause`. diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index a392135c13..4e8674b4dc 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -14,17 +14,28 @@ * limitations under the License. */ +import { isError } from './assertion'; + /** @public */ export class CustomErrorBase extends Error { readonly cause?: Error; - constructor(message?: string, cause?: Error) { + constructor(message?: string, cause?: Error | unknown) { + let assignedCause: Error | undefined = undefined; + let fullMessage = message; - if (cause) { - if (fullMessage) { - fullMessage += `; caused by ${cause}`; + if (cause !== undefined) { + let causeStr; + if (isError(cause)) { + assignedCause = cause; + causeStr = String(cause); } else { - fullMessage = `caused by ${cause}`; + causeStr = `unknown error '${cause}'`; + } + if (fullMessage) { + fullMessage += `; caused by ${causeStr}`; + } else { + fullMessage = `caused by ${causeStr}`; } } @@ -33,6 +44,6 @@ export class CustomErrorBase extends Error { Error.captureStackTrace?.(this, this.constructor); this.name = this.constructor.name; - this.cause = cause; + this.cause = assignedCause; } } diff --git a/packages/errors/src/errors/assertion.ts b/packages/errors/src/errors/assertion.ts index a0b2d59c11..4e8d939461 100644 --- a/packages/errors/src/errors/assertion.ts +++ b/packages/errors/src/errors/assertion.ts @@ -18,6 +18,7 @@ export type ErrorLike = { name: string; message: string; stack?: string; + [unknownKeys: string]: unknown; }; export function isError(val: unknown): val is ErrorLike { diff --git a/packages/errors/src/errors/common.test.ts b/packages/errors/src/errors/common.test.ts index 232c3e78ef..1b57be2540 100644 --- a/packages/errors/src/errors/common.test.ts +++ b/packages/errors/src/errors/common.test.ts @@ -18,7 +18,8 @@ import * as errors from './common'; describe('common', () => { it('extends Error properly', () => { - for (const [name, E] of Object.entries(errors)) { + const { ForwardedError: _, ...optionalCauseErrors } = { ...errors }; + for (const [name, E] of Object.entries(optionalCauseErrors)) { const error = new E('abcdef'); expect(error.name).toBe(name); expect(error.message).toBe('abcdef'); diff --git a/packages/errors/src/errors/common.ts b/packages/errors/src/errors/common.ts index dad236d4b5..f9300b9371 100644 --- a/packages/errors/src/errors/common.ts +++ b/packages/errors/src/errors/common.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { isError } from './assertion'; import { CustomErrorBase } from './CustomErrorBase'; /* @@ -72,3 +73,19 @@ export class ConflictError extends CustomErrorBase {} * @public */ export class NotModifiedError extends CustomErrorBase {} + +/** + * An error that forwards an underlying cause with additional context in the message. + * + * The `name` property of the error will be inherited from the `cause` if + * possible, and will otherwise be set to `'Error'`. + * + * @public + */ +export class ForwardedError extends CustomErrorBase { + constructor(message: string, cause: Error | unknown) { + super(message, cause); + + this.name = isError(cause) ? this.constructor.name : 'Error'; + } +} diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts index 02670f4de9..bc2fb2aa4e 100644 --- a/packages/errors/src/errors/index.ts +++ b/packages/errors/src/errors/index.ts @@ -19,6 +19,7 @@ export type { ErrorLike } from './assertion'; export { AuthenticationError, ConflictError, + ForwardedError, InputError, NotAllowedError, NotFoundError,