Skip to content

Commit 64b07dc

Browse files
authored
Merge pull request #599 from mCodex/fix/tsError
Make Error 'cause' non-enumerable and add tests
2 parents 1211c3c + 1f9ded6 commit 64b07dc

8 files changed

Lines changed: 120 additions & 19 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/**'

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"sideEffects": false,
1111
"exports": {
1212
".": {
13-
"react-native": "./src/index.ts",
1413
"import": {
1514
"types": "./lib/typescript/module/src/index.d.ts",
1615
"default": "./lib/module/index.js"
@@ -22,7 +21,6 @@
2221
"default": "./lib/module/index.js"
2322
},
2423
"./hooks": {
25-
"react-native": "./src/hooks/index.ts",
2624
"import": {
2725
"types": "./lib/typescript/module/src/hooks/index.d.ts",
2826
"default": "./lib/module/hooks/index.js"
@@ -34,7 +32,6 @@
3432
"default": "./lib/module/hooks/index.js"
3533
},
3634
"./errors": {
37-
"react-native": "./src/errors.ts",
3835
"import": {
3936
"types": "./lib/typescript/module/src/errors.d.ts",
4037
"default": "./lib/module/errors.js"

src/__tests__/errors.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,51 @@ 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('defines a non-enumerable own cause when explicitly passed as undefined', () => {
92+
const err = new SensitiveInfoError(ErrorCode.NotFound, 'wrapped', {
93+
cause: undefined,
94+
})
95+
const descriptor = Object.getOwnPropertyDescriptor(err, 'cause')
96+
expect(Object.hasOwn(err, 'cause')).toBe(true)
97+
expect(err.cause).toBeUndefined()
98+
expect(descriptor).toBeDefined()
99+
expect(descriptor?.enumerable).toBe(false)
100+
expect(Object.keys(err)).not.toContain('cause')
101+
})
102+
103+
it('propagates cause through subclasses (NotFoundError)', () => {
104+
const cause = new Error('native miss')
105+
const err = new NotFoundError('missing', { cause })
106+
expect(err.cause).toBe(cause)
107+
})
108+
})
64109
})
65110

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

src/__tests__/hooks.types.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,32 @@ 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+
38+
it('defines a non-enumerable own cause when explicitly passed as undefined', () => {
39+
const error = new HookError('Wrapper message', { cause: undefined })
40+
const descriptor = Object.getOwnPropertyDescriptor(error, 'cause')
41+
expect(Object.hasOwn(error, 'cause')).toBe(true)
42+
expect(error.cause).toBeUndefined()
43+
expect(descriptor).toBeDefined()
44+
expect(descriptor?.enumerable).toBe(false)
45+
expect(Object.keys(error)).not.toContain('cause')
46+
})
47+
2248
it('creates the initial async state', () => {
2349
const state = createInitialAsyncState<string>()
2450
expect(state).toEqual({

src/errors.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ export class SensitiveInfoError extends Error {
6262
/** Stable discriminant identifying the failure mode. */
6363
readonly code: ErrorCodeValue
6464

65+
/**
66+
* The underlying cause forwarded to {@link Error.cause}.
67+
*
68+
* Declared as a type-only member so the property type-checks under `tsconfig`
69+
* `lib` targets that predate ES2022 (where {@link Error} did not yet expose
70+
* `cause`). At runtime the value is installed by the constructor via
71+
* {@link Object.defineProperty} so it stays non-enumerable.
72+
*/
73+
declare readonly cause?: unknown
74+
6575
/**
6676
* @param code - Stable {@link ErrorCodeValue} for the failure.
6777
* @param message - Human-readable description.
@@ -75,11 +85,17 @@ export class SensitiveInfoError extends Error {
7585
super(message)
7686
this.name = 'SensitiveInfoError'
7787
this.code = code
78-
// Assign `cause` directly instead of passing it to `super()` so this
88+
// Define `cause` manually instead of passing it to `super()` so this
7989
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
80-
// the second `Error` constructor argument was introduced).
90+
// the second `Error` constructor argument was introduced), while keeping
91+
// the property non-enumerable to match the native ES2022 `Error` constructor.
8192
if (options && 'cause' in options) {
82-
;(this as { cause?: unknown }).cause = options.cause
93+
Object.defineProperty(this, 'cause', {
94+
value: options.cause,
95+
writable: true,
96+
configurable: true,
97+
enumerable: false,
98+
})
8399
}
84100
}
85101
}

src/hooks/types.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,40 @@ export class HookError extends Error {
3030
/** UI-facing remediation hint (e.g. `'Ask the user to retry biometrics.'`). */
3131
readonly hint?: string | undefined
3232

33+
/**
34+
* The underlying cause forwarded to {@link Error.cause}.
35+
*
36+
* Declared as a type-only member so the property type-checks under `tsconfig`
37+
* `lib` targets that predate ES2022 (where {@link Error} did not yet expose
38+
* `cause`). At runtime the value is installed by the constructor via
39+
* {@link Object.defineProperty} so it stays non-enumerable.
40+
*/
41+
declare readonly cause?: unknown
42+
3343
/**
3444
* @param message - Human-readable description of the failure.
3545
* @param options - Additional metadata; see {@link HookErrorOptions}.
3646
*/
37-
constructor(
38-
message: string,
39-
{ cause, operation, hint }: HookErrorOptions = {}
40-
) {
47+
constructor(message: string, options: HookErrorOptions = {}) {
4148
super(message)
4249
this.name = 'HookError'
43-
this.operation = operation
44-
this.hint = hint
45-
// Assign `cause` directly instead of passing it to `super()` so this
50+
this.operation = options.operation
51+
this.hint = options.hint
52+
// Define `cause` manually instead of passing it to `super()` so this
4653
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
47-
// the second `Error` constructor argument was introduced).
48-
if (cause !== undefined) {
49-
;(this as { cause?: unknown }).cause = cause
54+
// the second `Error` constructor argument was introduced), while keeping
55+
// the property non-enumerable to match the native ES2022 `Error` constructor.
56+
//
57+
// We deliberately use `'cause' in options` (rather than a value check)
58+
// so that `new HookError(msg, { cause: undefined })` still installs the
59+
// property — matching native `new Error(msg, { cause: undefined })`.
60+
if ('cause' in options) {
61+
Object.defineProperty(this, 'cause', {
62+
value: options.cause,
63+
writable: true,
64+
configurable: true,
65+
enumerable: false,
66+
})
5067
}
5168
}
5269
}

0 commit comments

Comments
 (0)