Skip to content

Commit f6a096e

Browse files
committed
fix(forms): Update a Forms validator error to use RuntimeError (#46537)
Replace `new Error()` in a forms Validators function with `RuntimeError`, for better tree-shakability. Also, improve the error messages, and add documentation. PR Close #46537
1 parent a322b7d commit f6a096e

File tree

7 files changed

+54
-13
lines changed

7 files changed

+54
-13
lines changed

aio/content/errors/NG1003.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@name Wrong Async Validator Return Type
2+
@category forms
3+
@shortDescription Async validator must return a Promise or Observable
4+
5+
@description
6+
Async validators must return a promise or an observable, and emit/resolve them whether the validation fails or succeeds. In particular, they must implement the [AsyncValidatorFn API](api/forms/AsyncValidator)
7+
8+
```typescript
9+
export function isTenAsync(control: AbstractControl):
10+
Observable<ValidationErrors> | null {
11+
const v: number = control.value;
12+
if (v !== 10) {
13+
// Emit an object with a validation error.
14+
return of({ 'notTen': true, 'requiredValue': 10 });
15+
}
16+
// Emit null, to indicate no error occurred.
17+
return of(null);
18+
}
19+
```
20+
21+
@debugging
22+
Did you mistakenly use a synchronous validator instead of an async validator?
23+
24+
<!-- links -->
25+
26+
<!-- external links -->
27+
28+
<!-- end links -->
29+
30+
@reviewed 2022-06-28

goldens/public-api/forms/errors.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export const enum RuntimeErrorCode {
1111
// (undocumented)
1212
MISSING_CONTROL_VALUE = 1002,
1313
// (undocumented)
14-
NO_CONTROLS = 1000
14+
NO_CONTROLS = 1000,
15+
// (undocumented)
16+
WRONG_VALIDATOR_RETURN_TYPE = -1101
1517
}
1618

1719
// (No @packageDocumentation comment for this package)

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,9 +1133,6 @@
11331133
{
11341134
"name": "isObject"
11351135
},
1136-
{
1137-
"name": "isObservable"
1138-
},
11391136
{
11401137
"name": "isOptionsObj"
11411138
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,9 +1094,6 @@
10941094
{
10951095
"name": "isObject"
10961096
},
1097-
{
1098-
"name": "isObservable"
1099-
},
11001097
{
11011098
"name": "isOptionsObj"
11021099
},

packages/forms/src/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ export const enum RuntimeErrorCode {
1818
MISSING_CONTROL = 1001,
1919
MISSING_CONTROL_VALUE = 1002,
2020

21+
// Validators errors
22+
WRONG_VALIDATOR_RETURN_TYPE = -1101,
23+
2124
// Template-driven Forms errors (11xx)
2225
}

packages/forms/src/validators.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {InjectionToken, ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core';
9+
import {InjectionToken, ɵisObservable as isObservable, ɵisPromise as isPromise, ɵRuntimeError as RuntimeError} from '@angular/core';
1010
import {forkJoin, from, Observable} from 'rxjs';
1111
import {map} from 'rxjs/operators';
1212

1313
import {AsyncValidator, AsyncValidatorFn, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
14+
import {RuntimeErrorCode} from './errors';
1415
import {AbstractControl} from './model/abstract_model';
1516

17+
const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
18+
1619
function isEmptyInputValue(value: any): boolean {
1720
/**
1821
* Check if the object is a string or array before evaluating the length attribute.
@@ -567,10 +570,16 @@ function isPresent(o: any): boolean {
567570
return o != null;
568571
}
569572

570-
export function toObservable(r: any): Observable<any> {
571-
const obs = isPromise(r) ? from(r) : r;
572-
if (!(isObservable(obs)) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
573-
throw new Error(`Expected validator to return Promise or Observable.`);
573+
export function toObservable(value: any): Observable<any> {
574+
const obs = isPromise(value) ? from(value) : value;
575+
if (NG_DEV_MODE && !(isObservable(obs))) {
576+
let errorMessage = `Expected async validator to return Promise or Observable.`;
577+
// A synchronous validator will return object or null.
578+
if (typeof value === 'object') {
579+
errorMessage +=
580+
' Are you using a synchronous validator where an async validator is expected?';
581+
}
582+
throw new RuntimeError(RuntimeErrorCode.WRONG_VALIDATOR_RETURN_TYPE, errorMessage);
574583
}
575584
return obs;
576585
}

packages/forms/test/form_control_spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,10 @@ describe('FormControl', () => {
14671467
// test for the specific error since without the error check it would still throw an error
14681468
// but
14691469
// not a meaningful one
1470-
expect(fn).toThrowError(`Expected validator to return Promise or Observable.`);
1470+
expect(fn).toThrowError(
1471+
'NG01101: Expected async validator to return Promise or Observable. ' +
1472+
'Are you using a synchronous validator where an async validator is expected? ' +
1473+
'Find more at https://angular.io/errors/NG01101');
14711474
});
14721475

14731476
it('should not emit value change events when emitEvent = false', () => {

0 commit comments

Comments
 (0)