Skip to content

Commit 3a01d72

Browse files
kirjsAndrewKushnir
authored andcommitted
refactor(forms): convert Signal Forms errors to use RuntimeError
- Added 13 new error codes to forms/src/errors.ts (1900-1999) - use RuntimeError
1 parent 915eee3 commit 3a01d72

File tree

9 files changed

+113
-31
lines changed

9 files changed

+113
-31
lines changed

packages/forms/signals/compat/src/compat_structure.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {computed, Signal, signal, WritableSignal} from '@angular/core';
9+
import {
10+
computed,
11+
Signal,
12+
signal,
13+
WritableSignal,
14+
ɵRuntimeError as RuntimeError,
15+
} from '@angular/core';
16+
import {SignalFormsErrorCode} from '../../src/errors';
1017
import {FormFieldManager} from '../../src/field/manager';
1118
import {FieldNode, ParentFieldNode} from '../../src/field/node';
1219
import {
@@ -111,7 +118,10 @@ export class CompatStructure extends FieldNodeStructure {
111118

112119
constructor(node: FieldNode, options: CompatFieldNodeOptions) {
113120
super(options.logic, node, () => {
114-
throw new Error(`Compat nodes don't have children.`);
121+
throw new RuntimeError(
122+
SignalFormsErrorCode.COMPAT_NO_CHILDREN,
123+
ngDevMode && `Compat nodes don't have children.`,
124+
);
115125
});
116126
this.value = getControlValueSignal(options);
117127
this.parent = getParentFromOptions(options);

packages/forms/signals/src/controls/interop_ng_control.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {ɵRuntimeError as RuntimeError} from '@angular/core';
10+
import {SignalFormsErrorCode} from '../errors';
11+
912
import {
1013
ControlValueAccessor,
1114
Validators,
@@ -122,7 +125,10 @@ export class InteropNgControl
122125
if (this.field().pending()) {
123126
return 'PENDING';
124127
}
125-
throw Error('AssertionError: unknown form control status');
128+
throw new RuntimeError(
129+
SignalFormsErrorCode.UNKNOWN_STATUS,
130+
ngDevMode && 'Unknown form control status',
131+
);
126132
}
127133

128134
valueAccessor: ControlValueAccessor | null = null;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* The list of error codes used in runtime code of the `forms` package.
11+
* Reserved error code range: 1900-1999.
12+
*/
13+
export const enum SignalFormsErrorCode {
14+
// Signal Forms errors (1900-1999)
15+
PATH_NOT_IN_FIELD_TREE = 1900,
16+
PATH_RESOLUTION_FAILED = 1901,
17+
ORPHAN_FIELD_PROPERTY = 1902,
18+
ORPHAN_FIELD_ARRAY = 1903,
19+
ORPHAN_FIELD_NOT_FOUND = 1904,
20+
ROOT_FIELD_NO_PARENT = 1905,
21+
PARENT_NOT_ARRAY = 1906,
22+
ABSTRACT_CONTROL_IN_FORM = 1907,
23+
PATH_OUTSIDE_SCHEMA = 1908,
24+
UNKNOWN_BUILDER_TYPE = 1909,
25+
UNKNOWN_STATUS = 1910,
26+
COMPAT_NO_CHILDREN = 1911,
27+
}

packages/forms/signals/src/field/context.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {computed, Signal, untracked, WritableSignal} from '@angular/core';
9+
import {
10+
computed,
11+
Signal,
12+
untracked,
13+
WritableSignal,
14+
ɵRuntimeError as RuntimeError,
15+
} from '@angular/core';
16+
import {SignalFormsErrorCode} from '../errors';
1017
import {AbstractControl} from '@angular/forms';
1118
import {
1219
FieldContext,
@@ -65,7 +72,10 @@ export class FieldNodeContext implements FieldContext<unknown> {
6572
stepsRemaining--;
6673
field = field.structure.parent;
6774
if (field === undefined) {
68-
throw new Error('Path is not part of this field tree.');
75+
throw new RuntimeError(
76+
SignalFormsErrorCode.PATH_NOT_IN_FIELD_TREE,
77+
ngDevMode && 'Path is not part of this field tree.',
78+
);
6979
}
7080
}
7181

@@ -74,11 +84,13 @@ export class FieldNodeContext implements FieldContext<unknown> {
7484
for (let key of targetPathNode.keys) {
7585
field = field.structure.getChild(key);
7686
if (field === undefined) {
77-
throw new Error(
78-
`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${[
79-
'<root>',
80-
...this.node.structure.pathKeys(),
81-
].join('.')}.`,
87+
throw new RuntimeError(
88+
SignalFormsErrorCode.PATH_RESOLUTION_FAILED,
89+
ngDevMode &&
90+
`Cannot resolve path .${targetPathNode.keys.join('.')} relative to field ${[
91+
'<root>',
92+
...this.node.structure.pathKeys(),
93+
].join('.')}.`,
8294
);
8395
}
8496
}
@@ -116,7 +128,10 @@ export class FieldNodeContext implements FieldContext<unknown> {
116128
const key = this.key();
117129
// Assert that the parent is actually an array.
118130
if (!isArray(untracked(this.node.structure.parent!.value))) {
119-
throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
131+
throw new RuntimeError(
132+
SignalFormsErrorCode.PARENT_NOT_ARRAY,
133+
ngDevMode && 'Cannot access index, parent field is not an array.',
134+
);
120135
}
121136
// Return the key as a number if we are indeed inside an array field.
122137
return Number(key);
@@ -128,8 +143,10 @@ export class FieldNodeContext implements FieldContext<unknown> {
128143
const result = this.resolve(p)().value();
129144

130145
if (result instanceof AbstractControl) {
131-
throw new Error(
132-
`Tried to read an 'AbstractControl' value form a 'form()'. Did you mean to use 'compatForm()' instead?`,
146+
throw new RuntimeError(
147+
SignalFormsErrorCode.ABSTRACT_CONTROL_IN_FORM,
148+
ngDevMode &&
149+
`Tried to read an 'AbstractControl' value from a 'form()'. Did you mean to use 'compatForm()' instead?`,
133150
);
134151
}
135152
return result;

packages/forms/signals/src/field/structure.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import {
1414
Signal,
1515
untracked,
1616
WritableSignal,
17+
ɵRuntimeError as RuntimeError,
1718
} from '@angular/core';
1819

20+
import {SignalFormsErrorCode} from '../errors';
21+
1922
import {LogicNode} from '../schema/logic_node';
2023
import type {FieldPathNode} from '../schema/path_node';
2124
import {deepSignal} from '../util/deep_signal';
@@ -186,8 +189,10 @@ export abstract class FieldNodeStructure {
186189
const key = initialKeyInParent!;
187190
return computed(() => {
188191
if (this.parent!.structure.getChild(key) !== this.node) {
189-
throw new Error(
190-
`RuntimeError: orphan field, looking for property '${key}' of ${getDebugName(this.parent!)}`,
192+
throw new RuntimeError(
193+
SignalFormsErrorCode.ORPHAN_FIELD_PROPERTY,
194+
ngDevMode &&
195+
`Orphan field, looking for property '${key}' of ${getDebugName(this.parent!)}`,
191196
);
192197
}
193198
return key;
@@ -204,8 +209,9 @@ export abstract class FieldNodeStructure {
204209
// It should not be possible to encounter this error. It would require the parent to
205210
// change from an array field to non-array field. However, in the current implementation
206211
// a field's parent can never change.
207-
throw new Error(
208-
`RuntimeError: orphan field, expected ${getDebugName(this.parent!)} to be an array`,
212+
throw new RuntimeError(
213+
SignalFormsErrorCode.ORPHAN_FIELD_ARRAY,
214+
ngDevMode && `Orphan field, expected ${getDebugName(this.parent!)} to be an array`,
209215
);
210216
}
211217

@@ -233,8 +239,9 @@ export abstract class FieldNodeStructure {
233239
}
234240
}
235241

236-
throw new Error(
237-
`RuntimeError: orphan field, can't find element in array ${getDebugName(this.parent!)}`,
242+
throw new RuntimeError(
243+
SignalFormsErrorCode.ORPHAN_FIELD_NOT_FOUND,
244+
ngDevMode && `Orphan field, can't find element in array ${getDebugName(this.parent!)}`,
238245
);
239246
});
240247
}
@@ -512,7 +519,10 @@ const ROOT_PATH_KEYS = computed<readonly string[]>(() => []);
512519
* do not have a parent. This signal will throw if it is read.
513520
*/
514521
const ROOT_KEY_IN_PARENT = computed(() => {
515-
throw new Error(`RuntimeError: the top-level field in the form has no parent`);
522+
throw new RuntimeError(
523+
SignalFormsErrorCode.ROOT_FIELD_NO_PARENT,
524+
ngDevMode && 'The top-level field in the form has no parent.',
525+
);
516526
});
517527

518528
/** Gets a human readable name for a field node for use in error messages. */

packages/forms/signals/src/schema/logic_node.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type {MetadataKey} from '../api/rules/metadata';
10-
import type {ValidationError} from '../api/rules/validation/validation_errors';
9+
import {ɵRuntimeError as RuntimeError} from '@angular/core';
10+
import {SignalFormsErrorCode} from '../errors';
11+
12+
import type {ValidationError, MetadataKey} from '../api/rules';
1113
import type {AsyncValidationResult, DisabledReason, LogicFn, ValidationResult} from '../api/types';
1214
import {setBoundPathDepthForResolution} from '../field/resolution';
1315
import {type BoundPredicate, DYNAMIC, LogicContainer, type Predicate} from './logic';
@@ -433,7 +435,10 @@ function getAllChildBuilders(
433435
...(builder.children.has(key) ? [{builder: builder.getChild(key), predicates: []}] : []),
434436
];
435437
} else {
436-
throw new Error('Unknown LogicNodeBuilder type');
438+
throw new RuntimeError(
439+
SignalFormsErrorCode.UNKNOWN_BUILDER_TYPE,
440+
ngDevMode && 'Unknown LogicNodeBuilder type',
441+
);
437442
}
438443
}
439444

@@ -467,7 +472,10 @@ function createLogic(
467472
} else if (builder instanceof NonMergeableLogicNodeBuilder) {
468473
logic.mergeIn(builder.logic);
469474
} else {
470-
throw new Error('Unknown LogicNodeBuilder type');
475+
throw new RuntimeError(
476+
SignalFormsErrorCode.UNKNOWN_BUILDER_TYPE,
477+
ngDevMode && 'Unknown LogicNodeBuilder type',
478+
);
471479
}
472480
return logic;
473481
}

packages/forms/signals/src/schema/schema.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {ɵRuntimeError as RuntimeError} from '@angular/core';
10+
import {SignalFormsErrorCode} from '../errors';
11+
912
import {SchemaPath, SchemaFn, SchemaOrSchemaFn} from '../api/types';
1013
import {FieldPathNode} from './path_node';
1114

@@ -105,9 +108,10 @@ export function isSchemaOrSchemaFn(value: unknown): value is SchemaOrSchemaFn<un
105108
/** Checks that a path node belongs to the schema function currently being compiled. */
106109
export function assertPathIsCurrent(path: SchemaPath<unknown>): void {
107110
if (currentCompilingNode !== FieldPathNode.unwrapFieldPath(path).root) {
108-
throw new Error(
109-
`A FieldPath can only be used directly within the Schema that owns it,` +
110-
` **not** outside of it or within a sub-schema.`,
111+
throw new RuntimeError(
112+
SignalFormsErrorCode.PATH_OUTSIDE_SCHEMA,
113+
ngDevMode &&
114+
`A FieldPath can only be used directly within the Schema that owns it, **not** outside of it or within a sub-schema.`,
111115
);
112116
}
113117
}

packages/forms/signals/test/node/field_context.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('Field Context', () => {
8787
);
8888
f().valid();
8989
expect(keys).toEqual([
90-
'RuntimeError: the top-level field in the form has no parent',
90+
jasmine.stringContaining('NG01905'), // SIGNAL_FORMS_ROOT_FIELD_NO_PARENT
9191
'name',
9292
'age',
9393
]);
@@ -125,10 +125,10 @@ describe('Field Context', () => {
125125
);
126126
f().valid();
127127
expect(indices).toEqual([
128-
'RuntimeError: the top-level field in the form has no parent',
128+
jasmine.stringContaining('NG01905'), // SIGNAL_FORMS_ROOT_FIELD_NO_PARENT
129129
0,
130130
1,
131-
'RuntimeError: cannot access index, parent field is not an array',
131+
jasmine.stringContaining('NG01906'), // SIGNAL_FORMS_PARENT_NOT_ARRAY
132132
]);
133133
});
134134

packages/forms/signals/test/node/field_node.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1319,7 +1319,7 @@ describe('FieldNode', () => {
13191319
{injector: TestBed.inject(Injector)},
13201320
);
13211321

1322-
expect(() => f().disabled()).toThrowError('Path is not part of this field tree.');
1322+
expect(() => f().disabled()).toThrowError(/Path is not part of this field tree\./);
13231323
});
13241324
});
13251325

0 commit comments

Comments
 (0)