From d037de5c78992a5ce87819bb1dca1b975ee693a3 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Wed, 4 Feb 2026 13:12:41 +0800 Subject: [PATCH 1/6] feat: Add ensureError utility for converting unknown values to Error Co-Authored-By: Claude Opus 4.5 --- src/errors.test.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++++ src/errors.ts | 23 +++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/errors.test.ts b/src/errors.test.ts index 12d07c54b..0ff2544f3 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import { + ensureError, getErrorMessage, isErrorWithCode, isErrorWithMessage, @@ -320,3 +321,99 @@ describe('getErrorMessage', () => { expect(getErrorMessage(undefined)).toBe(''); }); }); + +describe('ensureError', () => { + it('returns Error instance unchanged', () => { + const originalError = new Error('original message'); + const result = ensureError(originalError); + + expect(result).toBe(originalError); + expect(result.message).toBe('original message'); + }); + + it('returns fs.promises-style error unchanged', async () => { + let originalError; + try { + await fs.promises.readFile('/tmp/nonexistent', 'utf8'); + } catch (error: unknown) { + originalError = error; + } + + const result = ensureError(originalError); + + expect(result).toBe(originalError); + }); + + it('converts string to Error with string as message', () => { + const result = ensureError('something went wrong'); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('something went wrong'); + }); + + it('converts number to Error', () => { + const result = ensureError(42); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('42'); + }); + + it('converts object to Error using String()', () => { + const result = ensureError({ some: 'object' }); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('[object Object]'); + }); + + it('handles null with descriptive message', () => { + const result = ensureError(null); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown error'); + }); + + it('handles undefined with descriptive message', () => { + const result = ensureError(undefined); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown error'); + }); + + it('appends context to message for null', () => { + const result = ensureError(null, 'fetchData'); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown error (fetchData)'); + }); + + it('appends context to message for undefined', () => { + const result = ensureError(undefined, 'processInput'); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Unknown error (processInput)'); + }); + + it('appends context for non-Error values', () => { + const result = ensureError('network failure', 'apiCall'); + + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('network failure (apiCall)'); + }); + + it('does NOT add context to existing Error instances', () => { + const originalError = new Error('original message'); + const result = ensureError(originalError, 'someContext'); + + expect(result).toBe(originalError); + expect(result.message).toBe('original message'); + }); + + it('preserves stack trace for Error instances', () => { + const originalError = new Error('original message'); + const originalStack = originalError.stack; + + const result = ensureError(originalError); + + expect(result.stack).toBe(originalStack); + }); +}); diff --git a/src/errors.ts b/src/errors.ts index 72e6dc22c..73f4f4cbc 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -119,3 +119,26 @@ export function wrapError( return new Error(String(originalError)); } + +/** + * Ensures we have a proper Error object. + * If the input is already an Error, returns it unchanged. + * Otherwise, converts to an Error with an appropriate message. + * + * @param error - The caught error (could be Error, string, or unknown). + * @param context - Optional context to help identify the error source. + * @returns A proper Error instance. + */ +export function ensureError(error: unknown, context?: string): Error { + if (isError(error)) { + return error; + } + + if (isNullOrUndefined(error)) { + const message = context ? `Unknown error (${context})` : 'Unknown error'; + return new Error(message); + } + + const message = context ? `${String(error)} (${context})` : String(error); + return new Error(message); +} From ffab427c55f5d1cbb9bf065ac4e31251bd5a31f2 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Wed, 4 Feb 2026 16:33:53 +0800 Subject: [PATCH 2/6] test: Update export snapshots to include ensureError Co-Authored-By: Claude Opus 4.5 --- src/index.test.ts | 1 + src/node.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 9fd22c9d5..6204787fa 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -90,6 +90,7 @@ describe('index', () => { "createNumber", "createProjectLogger", "definePattern", + "ensureError", "exactOptional", "fromWei", "getChecksumAddress", diff --git a/src/node.test.ts b/src/node.test.ts index 1532d9c33..c3b927fd7 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -93,6 +93,7 @@ describe('node', () => { "definePattern", "directoryExists", "ensureDirectoryStructureExists", + "ensureError", "exactOptional", "fileExists", "forceRemove", From afe9d3337724044f59cd43732457fb605bd98775 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Thu, 5 Feb 2026 08:20:36 +0800 Subject: [PATCH 3/6] refactor: Simplify ensureError to use cause instead of string conversion - Use `{ cause: error }` to preserve original value instead of string conversion - Standardize message to "Unknown error" for all non-Error values - Remove unnecessary number-to-error test case - Update tests to verify cause preservation Co-Authored-By: Claude Opus 4.5 --- src/errors.test.ts | 33 +++++++++++++++++---------------- src/errors.ts | 12 ++++-------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/errors.test.ts b/src/errors.test.ts index 0ff2544f3..2f7b01027 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -344,39 +344,37 @@ describe('ensureError', () => { expect(result).toBe(originalError); }); - it('converts string to Error with string as message', () => { + it('converts string to Error and preserves original as cause', () => { const result = ensureError('something went wrong'); expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('something went wrong'); - }); - - it('converts number to Error', () => { - const result = ensureError(42); - - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('42'); + expect(result.message).toBe('Unknown error'); + expect(result.cause).toBe('something went wrong'); }); - it('converts object to Error using String()', () => { - const result = ensureError({ some: 'object' }); + it('converts object to Error and preserves original as cause', () => { + const originalObject = { some: 'object' }; + const result = ensureError(originalObject); expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('[object Object]'); + expect(result.message).toBe('Unknown error'); + expect(result.cause).toBe(originalObject); }); - it('handles null with descriptive message', () => { + it('handles null with descriptive message and preserves as cause', () => { const result = ensureError(null); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error'); + expect(result.cause).toBeNull(); }); - it('handles undefined with descriptive message', () => { + it('handles undefined with descriptive message and preserves as cause', () => { const result = ensureError(undefined); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error'); + expect(result.cause).toBeUndefined(); }); it('appends context to message for null', () => { @@ -384,6 +382,7 @@ describe('ensureError', () => { expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error (fetchData)'); + expect(result.cause).toBeNull(); }); it('appends context to message for undefined', () => { @@ -391,13 +390,15 @@ describe('ensureError', () => { expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error (processInput)'); + expect(result.cause).toBeUndefined(); }); - it('appends context for non-Error values', () => { + it('appends context for non-Error values and preserves original as cause', () => { const result = ensureError('network failure', 'apiCall'); expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('network failure (apiCall)'); + expect(result.message).toBe('Unknown error (apiCall)'); + expect(result.cause).toBe('network failure'); }); it('does NOT add context to existing Error instances', () => { diff --git a/src/errors.ts b/src/errors.ts index 73f4f4cbc..55a0cece8 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -123,7 +123,8 @@ export function wrapError( /** * Ensures we have a proper Error object. * If the input is already an Error, returns it unchanged. - * Otherwise, converts to an Error with an appropriate message. + * Otherwise, converts to an Error with an appropriate message and preserves + * the original value as the cause. * * @param error - The caught error (could be Error, string, or unknown). * @param context - Optional context to help identify the error source. @@ -134,11 +135,6 @@ export function ensureError(error: unknown, context?: string): Error { return error; } - if (isNullOrUndefined(error)) { - const message = context ? `Unknown error (${context})` : 'Unknown error'; - return new Error(message); - } - - const message = context ? `${String(error)} (${context})` : String(error); - return new Error(message); + const message = context ? `Unknown error (${context})` : 'Unknown error'; + return new Error(message, { cause: error }); } From 5355f209f7e247949abe15f16a71272e061ea3c1 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Thu, 5 Feb 2026 08:32:51 +0800 Subject: [PATCH 4/6] fix: Add ES2020 compatibility for Error cause in ensureError - Use ErrorWithCause polyfill when native Error cause is not supported - Follows same pattern as existing wrapError function - Add istanbul ignore for runtime-dependent branch Co-Authored-By: Claude Opus 4.5 --- src/errors.test.ts | 10 ++++++---- src/errors.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/errors.test.ts b/src/errors.test.ts index 2f7b01027..702c6beef 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -361,15 +361,16 @@ describe('ensureError', () => { expect(result.cause).toBe(originalObject); }); - it('handles null with descriptive message and preserves as cause', () => { + it('handles null with descriptive message', () => { const result = ensureError(null); expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error'); - expect(result.cause).toBeNull(); + // Note: ErrorWithCause polyfill converts null to undefined + expect(result.cause).toBeUndefined(); }); - it('handles undefined with descriptive message and preserves as cause', () => { + it('handles undefined with descriptive message', () => { const result = ensureError(undefined); expect(result).toBeInstanceOf(Error); @@ -382,7 +383,8 @@ describe('ensureError', () => { expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error (fetchData)'); - expect(result.cause).toBeNull(); + // Note: ErrorWithCause polyfill converts null to undefined + expect(result.cause).toBeUndefined(); }); it('appends context to message for undefined', () => { diff --git a/src/errors.ts b/src/errors.ts index 55a0cece8..486b9c9a1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -136,5 +136,15 @@ export function ensureError(error: unknown, context?: string): Error { } const message = context ? `Unknown error (${context})` : 'Unknown error'; - return new Error(message, { cause: error }); + + // Error causes are not supported by our current tsc target (ES2020, we need ES2022) + /* istanbul ignore if -- @preserve runtime-dependent branch */ + if (Error.length === 2) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new Error(message, { cause: error }); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new ErrorWithCause(message, { cause: error }); } From 152127606d71547853b5aa92d64d35aa715b925a Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Thu, 5 Feb 2026 22:29:42 +0800 Subject: [PATCH 5/6] fix: pr comments --- src/errors.test.ts | 6 ++---- src/errors.ts | 14 +++----------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/errors.test.ts b/src/errors.test.ts index 702c6beef..26f8c28f5 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -366,8 +366,7 @@ describe('ensureError', () => { expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error'); - // Note: ErrorWithCause polyfill converts null to undefined - expect(result.cause).toBeUndefined(); + expect(result.cause).toBeNull(); }); it('handles undefined with descriptive message', () => { @@ -383,8 +382,7 @@ describe('ensureError', () => { expect(result).toBeInstanceOf(Error); expect(result.message).toBe('Unknown error (fetchData)'); - // Note: ErrorWithCause polyfill converts null to undefined - expect(result.cause).toBeUndefined(); + expect(result.cause).toBeNull(); }); it('appends context to message for undefined', () => { diff --git a/src/errors.ts b/src/errors.ts index 486b9c9a1..47a24dcee 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -136,15 +136,7 @@ export function ensureError(error: unknown, context?: string): Error { } const message = context ? `Unknown error (${context})` : 'Unknown error'; - - // Error causes are not supported by our current tsc target (ES2020, we need ES2022) - /* istanbul ignore if -- @preserve runtime-dependent branch */ - if (Error.length === 2) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new Error(message, { cause: error }); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new ErrorWithCause(message, { cause: error }); + const newError: Error & { cause?: unknown } = new Error(message); + newError.cause = error; + return newError; } From 84582b161cf391ea83c00f85f344a2e137bb1b29 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Fri, 6 Feb 2026 15:32:24 +0800 Subject: [PATCH 6/6] fix: comments --- src/errors.test.ts | 32 -------------------------------- src/errors.ts | 6 ++---- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/errors.test.ts b/src/errors.test.ts index 26f8c28f5..b2c01d0c0 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -377,38 +377,6 @@ describe('ensureError', () => { expect(result.cause).toBeUndefined(); }); - it('appends context to message for null', () => { - const result = ensureError(null, 'fetchData'); - - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown error (fetchData)'); - expect(result.cause).toBeNull(); - }); - - it('appends context to message for undefined', () => { - const result = ensureError(undefined, 'processInput'); - - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown error (processInput)'); - expect(result.cause).toBeUndefined(); - }); - - it('appends context for non-Error values and preserves original as cause', () => { - const result = ensureError('network failure', 'apiCall'); - - expect(result).toBeInstanceOf(Error); - expect(result.message).toBe('Unknown error (apiCall)'); - expect(result.cause).toBe('network failure'); - }); - - it('does NOT add context to existing Error instances', () => { - const originalError = new Error('original message'); - const result = ensureError(originalError, 'someContext'); - - expect(result).toBe(originalError); - expect(result.message).toBe('original message'); - }); - it('preserves stack trace for Error instances', () => { const originalError = new Error('original message'); const originalStack = originalError.stack; diff --git a/src/errors.ts b/src/errors.ts index 47a24dcee..2bb8d8901 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -127,16 +127,14 @@ export function wrapError( * the original value as the cause. * * @param error - The caught error (could be Error, string, or unknown). - * @param context - Optional context to help identify the error source. * @returns A proper Error instance. */ -export function ensureError(error: unknown, context?: string): Error { +export function ensureError(error: unknown): Error { if (isError(error)) { return error; } - const message = context ? `Unknown error (${context})` : 'Unknown error'; - const newError: Error & { cause?: unknown } = new Error(message); + const newError: Error & { cause?: unknown } = new Error('Unknown error'); newError.cause = error; return newError; }