Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';

import {
ensureError,
getErrorMessage,
isErrorWithCode,
isErrorWithMessage,
Expand Down Expand Up @@ -320,3 +321,102 @@ 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 and preserves original as cause', () => {
const result = ensureError('something went wrong');

expect(result).toBeInstanceOf(Error);
expect(result.message).toBe('Unknown error');
expect(result.cause).toBe('something went wrong');
});

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('Unknown error');
expect(result.cause).toBe(originalObject);
});

it('handles null with descriptive message', () => {
const result = ensureError(null);

expect(result).toBeInstanceOf(Error);
expect(result.message).toBe('Unknown error');
// Note: ErrorWithCause polyfill converts null to undefined
expect(result.cause).toBeUndefined();
});

it('handles undefined with descriptive message', () => {
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', () => {
const result = ensureError(null, 'fetchData');

expect(result).toBeInstanceOf(Error);
expect(result.message).toBe('Unknown error (fetchData)');
// Note: ErrorWithCause polyfill converts null to undefined
expect(result.cause).toBeUndefined();
});

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;

const result = ensureError(originalError);

expect(result.stack).toBe(originalStack);
});
});
29 changes: 29 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,32 @@ export function wrapError<Throwable>(

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 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.
Comment thread
abretonc7s marked this conversation as resolved.
Outdated
* @returns A proper Error instance.
*/
export function ensureError(error: unknown, context?: string): Error {
if (isError(error)) {
return error;
}

const message = context ? `Unknown error (${context})` : 'Unknown error';

// Error causes are not supported by our current tsc target (ES2020, we need ES2022)
Comment thread
abretonc7s marked this conversation as resolved.
Outdated
/* istanbul ignore if -- @preserve runtime-dependent branch */
Comment thread
Gudahtt marked this conversation as resolved.
Outdated
if (Error.length === 2) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Comment thread
Gudahtt marked this conversation as resolved.
Outdated
return new Error(message, { cause: error });
}
Comment thread
abretonc7s marked this conversation as resolved.
Outdated
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return new ErrorWithCause(message, { cause: error });
}
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('index', () => {
"createNumber",
"createProjectLogger",
"definePattern",
"ensureError",
"exactOptional",
"fromWei",
"getChecksumAddress",
Expand Down
1 change: 1 addition & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('node', () => {
"definePattern",
"directoryExists",
"ensureDirectoryStructureExists",
"ensureError",
"exactOptional",
"fileExists",
"forceRemove",
Expand Down
Loading