Skip to content

Commit

Permalink
feat: Reuse native javascript cause property
Browse files Browse the repository at this point in the history
This commit aligns CError's api to more closely follow the ES2022 Error
API

BREAKING CHANGE: CError's static .cause() method was changed to .getCause(). Constructor
arguments were consolidated, so there are now two optional arguments
instead.
  • Loading branch information
jdpnielsen committed Sep 16, 2024
1 parent 3dc28bc commit 67575a8
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 75 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var filename = '/nonexistent';
try {
const stats = await fs.stat(filename);
} catch (err1) {
var err2 = new CError(`stat "${filename}"`, err1);
var err2 = new CError(`stat "${filename}"`, { cause: err1 });
console.error(err2.message);
}

Expand Down Expand Up @@ -87,8 +87,8 @@ kind of Error:

```typescript
const err1 = new Error('No such file or directory');
const err2 = new CError(`failed to stat ${filename}`, err1);
const err3 = new CError('request failed', err2);
const err2 = new CError(`failed to stat ${filename}`, { cause: err1 });
const err3 = new CError('request failed', { cause: err2 });
console.error(err3.message);
```

Expand Down Expand Up @@ -216,10 +216,10 @@ support first-class causes, informational properties and other useful features.

## Constructors

The CError constructor takes 3 optional arguments:
The CError constructor takes 2 optional arguments:

```typescript
new CError(message, cause, options)
new CError(message, options)
```

All of these forms construct a new CError that behaves just like the built-in
Expand All @@ -230,7 +230,8 @@ optional properties:

Option name | Type | Meaning
---------------- | ---------------- | -------
`name` | string | Describes what kind of error this is. This is intended for programmatic use to distinguish between different kinds of errors. Note that in modern versions of Node.js, this name is ignored in the `stack` property value, but callers can still use the `name` property to get at it.
`cause` | Error | Idicates the specific original cause of the error. See [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause)
`name` | string | Describes what kind of error this is. This is intended for programmatic use to distinguish between different kinds of errors. Note that in modern versions of Node.js, this name is ignored in the `stack` property value, but callers can still use the `name` property to get at it. See [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/name)
`info` | object | Specifies arbitrary informational properties that are available through the `CError.info(err)` static class method. See that method for details.

The `WError` constructor is used exactly the same way as the `CError`
Expand All @@ -244,6 +245,7 @@ JavaScript's built-in Error objects.
Property name | Type | Meaning
------------- | ------ | -------
`name` | string | Programmatically-usable name of the error.
`cause` | Error? | original cause of the error
`message` | string | Human-readable summary of the failure. Programmatically-accessible details are provided through `CError.info(err)` class method.
`stack` | string | Human-readable stack trace where the Error was constructed.

Expand All @@ -265,12 +267,12 @@ of CError or an instance of a class which inherited from CError. Under the hood,
the method uses a Symbol for checking, rather than `err instanceof CError`. This
allows compatability between versions of this library.

### `CError.cause(err)`
### `CError.getCause(err)`

The `cause()` function returns the next Error in the cause chain for `err`, or
The `getCause()` function returns the next Error in the cause chain for `err`, or
`null` if there is no next error. See the `cause` argument to the constructor.
Errors can have arbitrarily long cause chains. You can walk the `cause` chain
by invoking `CError.cause(err)` on each subsequent return value. If `err` is
by invoking `CError.getCause(err)` on each subsequent return value. If `err` is
not a `CError`, the cause is `null`.

### `CError.info(err)`
Expand Down Expand Up @@ -319,7 +321,8 @@ case:
```typescript
const err1 = new CError('something bad happened');
/* ... */
const err2 = new CError( `failed to connect to "${ip}:${port}"`, err1, {
const err2 = new CError( `failed to connect to "${ip}:${port}"`, {
'cause': err1,
'name': 'ConnectionError',
'info': {
'errno': 'ECONNREFUSED',
Expand Down Expand Up @@ -354,7 +357,8 @@ of the chain overriding same-named values lower in the chain. To continue that
example:

```typescript
const err3 = new CError('request failed', err2, {
const err3 = new CError('request failed', {
'cause': err2,
'name': 'RequestError',
'info': {
'errno': 'EBADREQUEST'
Expand Down Expand Up @@ -388,7 +392,7 @@ You can also print the complete stack trace of combined `Error`s by using
```typescript
var err1 = new CError('something bad happened');
/* ... */
var err2 = new CError('something really bad happened here', err1);
var err2 = new CError('something really bad happened here', { cause: err1 });

console.log(CError.fullStack(err2));
```
Expand Down
43 changes: 21 additions & 22 deletions src/lib/cerror.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function helperStack(message: string, name = 'CError') {
const nodestack = new Error().stack!.split('\n').slice(2).join('\n');

return [
`${name}: ${message}`,
message ? `${name}: ${message}` : name,
cleanStack(nodestack),
].join('\n');
}
Expand All @@ -51,23 +51,23 @@ test('should handle no input', t => {

test('should accept an Error as cause', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError);
const childError = new Error('ChildError', { cause: parentError });

t.is(childError.message, `ChildError: ParentError`, 'builds correct message');
t.is(childError.message, `ChildError`, 'builds correct message');
t.is((childError as any).cause, parentError, 'has expected cause');
});

test('should accept a CError as cause', t => {
const parentError = new CError('ParentError');
const childError = new CError('ChildError', parentError);
const childError = new CError('ChildError', { cause: parentError });

t.is(childError.message, `ChildError: ParentError`, 'builds correct message');
t.is((childError as any).cause, parentError, 'has expected cause');
});

test('should accept an options object', t => {
const uncausedErr = new CError('uncausedErr', undefined, { info: { text: '_uncausedErr_', fromUncaused: true }, name: 'UncausedError' });
const causedErr = new CError('causedErr', uncausedErr, { info: { text: '_causedErr_', fromCaused: true }, name: 'CausedError' });
const uncausedErr = new CError('uncausedErr', { info: { text: '_uncausedErr_', fromUncaused: true }, name: 'UncausedError' });
const causedErr = new CError('causedErr', { cause: uncausedErr, info: { text: '_causedErr_', fromCaused: true }, name: 'CausedError' });

t.is(uncausedErr.name, `UncausedError`, 'sets correct name');
t.is(causedErr.name, `CausedError`, 'sets correct name');
Expand All @@ -84,8 +84,8 @@ test('should accept an options object', t => {

test('should handle multiple nestings of causes', t => {
const parentError = new CError('ParentError');
const childError = new CError('ChildError', parentError);
const grandChildError = new CError('GrandChildError', childError);
const childError = new CError('ChildError', { cause: parentError });
const grandChildError = new CError('GrandChildError', { cause: childError });

t.is(grandChildError.message, `GrandChildError: ChildError: ParentError`, 'builds correct message');
t.is((grandChildError as any).cause, childError, 'has expected cause');
Expand All @@ -96,14 +96,14 @@ test('should handle multiple nestings of causes', t => {

test('should stringify correctly', t => {
const parentError = new CError('ParentError');
const childError = new CError('ChildError', parentError, { info: { foo: 'bar' }});
const childError = new CError('ChildError', { cause: parentError, info: { foo: 'bar' }});

t.is(childError.toString(), 'CError: ChildError: ParentError');
});

test('should JSON.stringify correctly', t => {
const parentError = new CError('ParentError');
const childError = new CError('ChildError', parentError, { info: { foo: 'bar' } });
const childError = new CError('ChildError', { cause: parentError, info: { foo: 'bar' } });

t.is(JSON.stringify(childError), '{"error":"CError","message":"ChildError"}');
});
Expand All @@ -119,16 +119,16 @@ test('should have a static .isCError method which returns true when given a CErr

test('should have a static .cause which returns the expected cause', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError, { info: { foo: 'bar' } });
const childError = new CError('ChildError', { cause: parentError, info: { foo: 'bar' } });

t.is(CError.cause(parentError), null, 'handles regular errors');
t.is(CError.cause(childError), parentError, 'returns cause');
t.is(CError.getCause(parentError), null, 'handles regular errors');
t.is(CError.getCause(childError), parentError, 'returns cause');
});

test('should have a static .info which returns the info object', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError, { info: { foo: 'bar' } });
const grandChildError = new CError('GrandChildError', childError, { info: { bar: 'baz' } });
const childError = new CError('ChildError', { cause: parentError, info: { foo: 'bar' } });
const grandChildError = new CError('GrandChildError', { cause: childError, info: { bar: 'baz' } });

t.deepEqual(CError.info(parentError), {}, 'handles regular errors');
t.deepEqual(CError.info(childError), { foo: 'bar' }, 'returns info');
Expand All @@ -137,9 +137,8 @@ test('should have a static .info which returns the info object', t => {

test('should have a static .fullStack which returns the combined stack trace', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError, { info: { foo: 'bar' } });
const grandChildError = new CError('GrandChildError', childError, { info: { bar: 'baz' } });

const childError = new CError('ChildError', { cause: parentError, info: { foo: 'bar' } });
const grandChildError = new CError('GrandChildError', { cause: childError, info: { bar: 'baz' } });
const expectedParentStack = helperStack('ParentError', 'Error');
const expectedChildStack = helperStack('ChildError: ParentError', 'CError') + '\ncaused by: ' + expectedParentStack;
const expectedGrandChildStack = helperStack('GrandChildError: ChildError: ParentError', 'CError') + '\ncaused by: ' + expectedChildStack;
Expand All @@ -151,8 +150,8 @@ test('should have a static .fullStack which returns the combined stack trace', t

test('should have a static .findCauseByName', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError);
const grandChildError = new CError('GrandChildError', childError, { name: 'CustomErrorName' });
const childError = new CError('ChildError', { cause: parentError });
const grandChildError = new CError('GrandChildError', { cause: childError, name: 'CustomErrorName' });

t.is(CError.findCauseByName(grandChildError, 'Error'), parentError, 'finds regular Error');
t.is(CError.findCauseByName(grandChildError, 'CError'), childError, 'finds CError');
Expand All @@ -162,8 +161,8 @@ test('should have a static .findCauseByName', t => {

test('should have a static .hasCauseWithName', t => {
const parentError = new Error('ParentError');
const childError = new CError('ChildError', parentError);
const grandChildError = new CError('GrandChildError', childError, { name: 'CustomErrorName' });
const childError = new CError('ChildError', { cause: parentError });
const grandChildError = new CError('GrandChildError', { cause: childError, name: 'CustomErrorName' });

t.is(CError.hasCauseWithName(grandChildError, 'Error'), true, 'finds regular Error');
t.is(CError.hasCauseWithName(grandChildError, 'CError'), true, 'finds CError');
Expand Down
33 changes: 14 additions & 19 deletions src/lib/cerror.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export type Info = Record<string | number | symbol, unknown>

export interface Options {
/**
* Indicates that the new error was caused by some other error
*/
cause?: Error | CError | unknown;

/**
* Specifies arbitrary informational properties that are available through the
* `ContextualError.info(err)` static class method. See that method for details.
Expand Down Expand Up @@ -34,7 +39,6 @@ export const CERROR_SYMBOL = Symbol.for('contextual-error/cerror');

export class CError extends Error {
public readonly name: string = 'CError';
public readonly message!: string;
public readonly info: Info = {};

/**
Expand All @@ -44,13 +48,9 @@ export class CError extends Error {
*/
public readonly shortMessage: string;

/**
* Indicates that the new error was caused by some other error
*/
private readonly cause?: Error | CError;
constructor(message?: string, options?: Options) {
super(message, { cause: options?.cause });

constructor(message?: string, cause?: Error, options?: Options) {
super(message);
Object.defineProperty(this, CERROR_SYMBOL, { value: true });
this.shortMessage = this.message;

Expand All @@ -70,14 +70,9 @@ export class CError extends Error {
this.name = options.name;
}

if (cause) {
Object.defineProperty(this, 'cause', {
value: cause,
enumerable: false,
});

if (!options?.skipCauseMessage) {
this.message += `: ${cause.message}`;
if (options?.cause) {
if (!options.skipCauseMessage && typeof (options.cause as Error)?.message === 'string') {
this.message += `: ${(options.cause as Error).message}`;
}
}

Expand Down Expand Up @@ -110,7 +105,7 @@ export class CError extends Error {
return (obj as {[CERROR_SYMBOL]?: boolean})?.[CERROR_SYMBOL] != null;
}

public static cause(err: CError | Error): CError | Error | null {
public static getCause(err: CError | Error): CError | Error | null {
if ((err as CError).cause) {
return (err as CError).cause as CError | Error;
} else {
Expand All @@ -119,7 +114,7 @@ export class CError extends Error {
}

public static info(err: CError | Error): Info {
const cause = CError.cause(err);
const cause = CError.getCause(err);
let info: Info;

if (cause !== null) {
Expand All @@ -142,7 +137,7 @@ export class CError extends Error {
* Returns a string containing the full stack trace, with all nested errors recursively reported as 'caused by:' + err.stack.
*/
public static fullStack(err: CError | Error): string {
const cause = CError.cause(err);
const cause = CError.getCause(err);

if (cause) {
return (err.stack + '\ncaused by: ' + CError.fullStack(cause));
Expand All @@ -155,7 +150,7 @@ export class CError extends Error {
public static findCauseByName(err: CError | Error, name: string): CError | Error | null {
let cause: CError | Error | null;

for (cause = err; cause !== null; cause = CError.cause(cause)) {
for (cause = err; cause !== null; cause = CError.getCause(cause)) {
if (cause.name == name) {
return cause;
}
Expand Down
Loading

0 comments on commit 67575a8

Please sign in to comment.