Skip to content

Commit 78f320b

Browse files
committed
Make Error 'cause' non-enumerable and add tests
Define the `cause` property with Object.defineProperty in SensitiveInfoError and HookError so it remains non-enumerable (matching native ES2022 Error semantics) while keeping compatibility with TS libs predating ES2022. Add tests to verify cause chaining, non-enumerability, and omission when not provided. Also update CI workflow triggers to use the 'master' branch for android, ios and test workflows.
1 parent e0626a7 commit 78f320b

7 files changed

Lines changed: 70 additions & 9 deletions

File tree

.github/workflows/android-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ permissions:
66
on:
77
push:
88
branches:
9-
- main
9+
- master
1010
paths:
1111
- '.github/workflows/android-build.yml'
1212
- 'example/android/**'

.github/workflows/ios-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ permissions:
66
on:
77
push:
88
branches:
9-
- main
9+
- master
1010
paths:
1111
- '.github/workflows/ios-build.yml'
1212
- 'example/ios/**'

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ permissions:
66
on:
77
push:
88
branches:
9-
- main
9+
- master
1010
paths:
1111
- '.github/workflows/test.yml'
1212
- 'src/**'

src/__tests__/errors.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,39 @@ describe('errors', () => {
6161
const err = new KeyInvalidatedError('invalid', { alias: 'rnsi.svc.v1' })
6262
expect(err.alias).toBe('rnsi.svc.v1')
6363
})
64+
65+
describe('cause chaining', () => {
66+
it('retains the provided cause on SensitiveInfoError', () => {
67+
const cause = new Error('underlying')
68+
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
69+
cause,
70+
})
71+
expect(err.cause).toBe(cause)
72+
})
73+
74+
it('keeps cause non-enumerable to match native ES2022 Error semantics', () => {
75+
const cause = new Error('underlying')
76+
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
77+
cause,
78+
})
79+
const descriptor = Object.getOwnPropertyDescriptor(err, 'cause')
80+
expect(descriptor).toBeDefined()
81+
expect(descriptor?.enumerable).toBe(false)
82+
expect(Object.keys(err)).not.toContain('cause')
83+
expect(JSON.parse(JSON.stringify(err))).not.toHaveProperty('cause')
84+
})
85+
86+
it('does not define cause when not provided', () => {
87+
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped')
88+
expect(Object.hasOwn(err, 'cause')).toBe(false)
89+
})
90+
91+
it('propagates cause through subclasses (NotFoundError)', () => {
92+
const cause = new Error('native miss')
93+
const err = new NotFoundError('missing', { cause })
94+
expect(err.cause).toBe(cause)
95+
})
96+
})
6497
})
6598

6699
describe('toSensitiveInfoError', () => {

src/__tests__/hooks.types.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ describe('hooks/types', () => {
1919
expect(error.hint).toBe('Check the key.')
2020
})
2121

22+
it('keeps cause non-enumerable to match native ES2022 Error semantics', () => {
23+
const cause = new Error('native failure')
24+
const error = new HookError('Wrapper message', { cause })
25+
26+
const descriptor = Object.getOwnPropertyDescriptor(error, 'cause')
27+
expect(descriptor).toBeDefined()
28+
expect(descriptor?.enumerable).toBe(false)
29+
expect(Object.keys(error)).not.toContain('cause')
30+
expect(JSON.parse(JSON.stringify(error))).not.toHaveProperty('cause')
31+
})
32+
33+
it('omits cause when not provided', () => {
34+
const error = new HookError('Wrapper message')
35+
expect(Object.hasOwn(error, 'cause')).toBe(false)
36+
})
37+
2238
it('creates the initial async state', () => {
2339
const state = createInitialAsyncState<string>()
2440
expect(state).toEqual({

src/errors.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,17 @@ export class SensitiveInfoError extends Error {
7575
super(message)
7676
this.name = 'SensitiveInfoError'
7777
this.code = code
78-
// Assign `cause` directly instead of passing it to `super()` so this
78+
// Define `cause` manually instead of passing it to `super()` so this
7979
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
80-
// the second `Error` constructor argument was introduced).
80+
// the second `Error` constructor argument was introduced), while keeping
81+
// the property non-enumerable to match the native ES2022 `Error` constructor.
8182
if (options && 'cause' in options) {
82-
;(this as { cause?: unknown }).cause = options.cause
83+
Object.defineProperty(this, 'cause', {
84+
value: options.cause,
85+
writable: true,
86+
configurable: true,
87+
enumerable: false,
88+
})
8389
}
8490
}
8591
}

src/hooks/types.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,17 @@ export class HookError extends Error {
4242
this.name = 'HookError'
4343
this.operation = operation
4444
this.hint = hint
45-
// Assign `cause` directly instead of passing it to `super()` so this
45+
// Define `cause` manually instead of passing it to `super()` so this
4646
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
47-
// the second `Error` constructor argument was introduced).
47+
// the second `Error` constructor argument was introduced), while keeping
48+
// the property non-enumerable to match the native ES2022 `Error` constructor.
4849
if (cause !== undefined) {
49-
;(this as { cause?: unknown }).cause = cause
50+
Object.defineProperty(this, 'cause', {
51+
value: cause,
52+
writable: true,
53+
configurable: true,
54+
enumerable: false,
55+
})
5056
}
5157
}
5258
}

0 commit comments

Comments
 (0)