Skip to content

Commit 1f9ded6

Browse files
committed
Support non-enumerable 'cause' on errors
Declare a type-only readonly `cause?: unknown` on HookError and SensitiveInfoError so TS configs that predate ES2022 type-check. Update HookError to accept an options object, install the `cause` property via Object.defineProperty when `'cause' in options` (so passing { cause: undefined } still creates a non-enumerable own property), and wire up operation/hint from the options. Add tests for HookError and SensitiveInfoError that assert a non-enumerable own `cause` is defined when explicitly passed as undefined. Also remove react-native-specific exports from package.json.
1 parent 78f320b commit 1f9ded6

5 files changed

Lines changed: 51 additions & 11 deletions

File tree

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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ describe('errors', () => {
8888
expect(Object.hasOwn(err, 'cause')).toBe(false)
8989
})
9090

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+
91103
it('propagates cause through subclasses (NotFoundError)', () => {
92104
const cause = new Error('native miss')
93105
const err = new NotFoundError('missing', { cause })

src/__tests__/hooks.types.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ describe('hooks/types', () => {
3535
expect(Object.hasOwn(error, 'cause')).toBe(false)
3636
})
3737

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+
3848
it('creates the initial async state', () => {
3949
const state = createInitialAsyncState<string>()
4050
expect(state).toEqual({

src/errors.ts

Lines changed: 10 additions & 0 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.

src/hooks/types.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,36 @@ 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
50+
this.operation = options.operation
51+
this.hint = options.hint
4552
// Define `cause` manually instead of passing it to `super()` so this
4653
// compiles cleanly under TS configs whose `lib` predates ES2022 (where
4754
// the second `Error` constructor argument was introduced), while keeping
4855
// the property non-enumerable to match the native ES2022 `Error` constructor.
49-
if (cause !== undefined) {
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) {
5061
Object.defineProperty(this, 'cause', {
51-
value: cause,
62+
value: options.cause,
5263
writable: true,
5364
configurable: true,
5465
enumerable: false,

0 commit comments

Comments
 (0)