Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
54 changes: 54 additions & 0 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @param defaultMessage The default message if no message is available.
*
* @returns The message or `null` if no message is available.
*
* @deprecated use {@link extractErrorDetails}
*/
export function extractErrorMessage(error: unknown, defaultMessage = 'Unknown error'): string {
if (error instanceof Error && error.message !== '') {
Expand All @@ -17,3 +19,55 @@ export function extractErrorMessage(error: unknown, defaultMessage = 'Unknown er

return defaultMessage;
}

/**
* Extracted details about an error value.
*/
export type ErrorDetails = {
/** The error message. */
message: string,

/** The stack trace of the error, if available. */
stack?: string,

/** The details of the error that caused this error, if part of a chain. */
cause?: ErrorDetails,
};

/**
* Extracts the details from an error of unknown type.
*
* If the error is a non-empty string, the string itself is used as the message.
* Otherwise, if the error is an `Error` with a non-empty message, its message, stack trace, and cause
* are extracted; the cause is extracted recursively using the same default message.
*
* @param error The error to extract the details.
* @param defaultMessage The default message if the error is not a non-empty string or an `Error` with a message.
*
* @returns The extracted details, including the stack trace and the cause, when available.
*/
export function extractErrorDetails(error: unknown, defaultMessage = 'Unknown error'): ErrorDetails {
if (typeof error === 'string' && error !== '') {
return {message: error};
}

if (
typeof error !== 'object'
|| !(error instanceof Error)
|| error.message === ''
) {
return {message: defaultMessage};
}

const details: ErrorDetails = {message: error.message};

if (error.stack !== undefined) {
details.stack = error.stack;
}

if (error.cause !== undefined) {
details.cause = extractErrorDetails(error.cause, defaultMessage);
Comment thread
Fryuni marked this conversation as resolved.
Outdated
}

return details;
}
90 changes: 88 additions & 2 deletions test/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-console -- Needed for testing */
import {extractErrorMessage} from '../src';
import {ErrorDetails, extractErrorDetails, extractErrorMessage} from '../src';

describe('A function for extracting error messages', () => {
it.each<[any, string|undefined, string]>([
Expand All @@ -20,3 +19,90 @@ describe('A function for extracting error messages', () => {
expect(extractErrorMessage(error, defaultMessage)).toBe(message);
});
});

describe('A function for extracting error details', () => {
it.each<[unknown, string|undefined, ErrorDetails]>([
['Error message.', undefined, {message: 'Error message.'}],
['', undefined, {message: 'Unknown error'}],
['', 'Custom default message', {message: 'Custom default message'}],
[null, undefined, {message: 'Unknown error'}],
[null, 'Custom default message', {message: 'Custom default message'}],
[true, undefined, {message: 'Unknown error'}],
[{}, undefined, {message: 'Unknown error'}],
[new Error(''), undefined, {message: 'Unknown error'}],
[new Error(''), 'Custom default message', {message: 'Custom default message'}],
])('should extract the details of the error %p as %j', (
error: unknown,
defaultMessage: string | undefined,
details: ErrorDetails,
) => {
expect(extractErrorDetails(error, defaultMessage)).toEqual(details);
});

it('should extract the message and stack of an error', () => {
const error = new Error('Error message.');

error.stack = 'Error: Error message.\n at <anonymous>:1:1';

expect(extractErrorDetails(error)).toEqual({
message: 'Error message.',
stack: 'Error: Error message.\n at <anonymous>:1:1',
});
});

it('should omit the stack when the error has none', () => {
const error = new Error('Error message.');

delete error.stack;

expect(extractErrorDetails(error)).toEqual({message: 'Error message.'});
});

it('should recursively extract the details of the cause', () => {
const cause = new Error('Cause message.');
const error = new Error('Error message.');

cause.stack = 'Error: Cause message.\n at <anonymous>:1:1';
error.stack = 'Error: Error message.\n at <anonymous>:2:2';
error.cause = cause;

expect(extractErrorDetails(error)).toEqual({
message: 'Error message.',
stack: 'Error: Error message.\n at <anonymous>:2:2',
cause: {
message: 'Cause message.',
stack: 'Error: Cause message.\n at <anonymous>:1:1',
},
});
});

it('should extract the message of a string cause', () => {
const error = new Error('Error message.');

error.stack = 'Error: Error message.\n at <anonymous>:1:1';
error.cause = 'Cause message.';

expect(extractErrorDetails(error)).toEqual({
message: 'Error message.',
stack: 'Error: Error message.\n at <anonymous>:1:1',
cause: {message: 'Cause message.'},
});
});

it.each<[unknown, string|undefined, string]>([
[new Error(''), undefined, 'Unknown error'],
[new Error(''), 'Custom default message', 'Custom default message'],
[{}, undefined, 'Unknown error'],
[{}, 'Custom default message', 'Custom default message'],
])('should fall back to "%s" for the unsupported cause %p', (
cause: unknown,
defaultMessage: string | undefined,
causeMessage: string,
) => {
const error = new Error('Error message.');

error.cause = cause;

expect(extractErrorDetails(error, defaultMessage).cause).toEqual({message: causeMessage});
});
});
Loading