Skip to content

Commit

Permalink
refactor(core): make RuntimeError reusable across packages (#44398) (#…
Browse files Browse the repository at this point in the history
…44652)

This commit updates the code around the `RuntimeError` class to make it more reusable between packages (currently it's only usable inside the `core` package). Specifically:
- the error formatting logic was updated to handle cases when there is no error message provided
- there is no special Set that contains a set of error codes for which we have guides on angular.io. Instead, this is now encoded into the error code itself (making such codes negative integers). Having a separate Set makes it non-tree-shakable, which we want to avoid.

This change should allow to employ the `RuntimeError` class in other packages to further standardize this subsystem and make the errors thrown by the framework consistent.

As a part of the refactoring, the `common` package code was also updated to follow the same logic as `core`, since the `RuntimeError` class was used there as well.

PR Close #44398

PR Close #44652
  • Loading branch information
AndrewKushnir authored and atscott committed Jan 7, 2022
1 parent 57d5f95 commit c4fbd85
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 53 deletions.
6 changes: 4 additions & 2 deletions packages/common/src/directives/ng_switch.ts
Expand Up @@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, DoCheck, Host, Input, Optional, TemplateRef, ViewContainerRef, ɵRuntimeError as RuntimeError, ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/core';
import {Directive, DoCheck, Host, Input, Optional, TemplateRef, ViewContainerRef, ɵRuntimeError as RuntimeError} from '@angular/core';

import {RuntimeErrorCode} from '../errors';

export class SwitchView {
private _created = false;
Expand Down Expand Up @@ -243,7 +245,7 @@ export class NgSwitchDefault {

function throwNgSwitchProviderNotFoundError(attrName: string, directiveName: string): never {
throw new RuntimeError(
RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR,
RuntimeErrorCode.PARENT_NG_SWITCH_NOT_FOUND,
`An element with the "${attrName}" attribute ` +
`(matching the "${
directiveName}" directive) must be located inside an element with the "ngSwitch" attribute ` +
Expand Down
16 changes: 16 additions & 0 deletions packages/common/src/errors.ts
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* The list of error codes used in runtime code of the `common` package.
* Reserved error code range: 2000-2999.
*/
export const enum RuntimeErrorCode {
// NgSwitch errors
PARENT_NG_SWITCH_NOT_FOUND = 2000,
}
4 changes: 2 additions & 2 deletions packages/common/test/directives/ng_switch_spec.ts
Expand Up @@ -182,7 +182,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';

expect(() => createTestComponent(template))
.toThrowError(
'NG0305: An element with the "ngSwitchCase" attribute (matching the "NgSwitchCase" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)');
'NG02000: An element with the "ngSwitchCase" attribute (matching the "NgSwitchCase" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)');
}));

it('should throw error when ngSwitchDefault is used outside of ngSwitch', waitForAsync(() => {
Expand All @@ -191,7 +191,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';

expect(() => createTestComponent(template))
.toThrowError(
'NG0305: An element with the "ngSwitchDefault" attribute (matching the "NgSwitchDefault" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)');
'NG02000: An element with the "ngSwitchDefault" attribute (matching the "NgSwitchDefault" directive) must be located inside an element with the "ngSwitch" attribute (matching "NgSwitch" directive)');
}));

it('should support nested NgSwitch on ng-container with ngTemplateOutlet', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Expand Up @@ -22,7 +22,7 @@ export {ComponentFactory as ɵComponentFactory} from './linker/component_factory
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {RuntimeError as ɵRuntimeError, RuntimeErrorCode as ɵRuntimeErrorCode} from './render3/error_code';
export {RuntimeError as ɵRuntimeError} from './render3/error_code';
export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
Expand Down
84 changes: 38 additions & 46 deletions packages/core/src/render3/error_code.ts
Expand Up @@ -8,75 +8,67 @@

import {ERROR_DETAILS_PAGE_BASE_URL} from './error_details_base_url';

/**
* The list of error codes used in runtime code of the `core` package.
* Reserved error code range: 100-999.
*
* Note: the minus sign denotes the fact that a particular code has a detailed guide on
* angular.io. This extra annotation is needed to avoid introducing a separate set to store
* error codes which have guides, which might leak into runtime code.
*
* Full list of available error guides can be found at https://angular.io/errors.
*/
export const enum RuntimeErrorCode {
// Internal Errors

// Change Detection Errors
EXPRESSION_CHANGED_AFTER_CHECKED = '100',
RECURSIVE_APPLICATION_REF_TICK = '101',
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
RECURSIVE_APPLICATION_REF_TICK = 101,

// Dependency Injection Errors
CYCLIC_DI_DEPENDENCY = '200',
PROVIDER_NOT_FOUND = '201',
CYCLIC_DI_DEPENDENCY = -200,
PROVIDER_NOT_FOUND = -201,

// Template Errors
MULTIPLE_COMPONENTS_MATCH = '300',
EXPORT_NOT_FOUND = '301',
PIPE_NOT_FOUND = '302',
UNKNOWN_BINDING = '303',
UNKNOWN_ELEMENT = '304',
TEMPLATE_STRUCTURE_ERROR = '305',
MULTIPLE_COMPONENTS_MATCH = -300,
EXPORT_NOT_FOUND = -301,
PIPE_NOT_FOUND = -302,
UNKNOWN_BINDING = 303,
UNKNOWN_ELEMENT = 304,
TEMPLATE_STRUCTURE_ERROR = 305,

// Bootstrap Errors
MULTIPLE_PLATFORMS = '400',
PLATFORM_NOT_FOUND = '401',
ERROR_HANDLER_NOT_FOUND = '402',
BOOTSTRAP_COMPONENTS_NOT_FOUND = '403',
ALREADY_DESTROYED_PLATFORM = '404',
ASYNC_INITIALIZERS_STILL_RUNNING = '405',
MULTIPLE_PLATFORMS = 400,
PLATFORM_NOT_FOUND = 401,
ERROR_HANDLER_NOT_FOUND = 402,
BOOTSTRAP_COMPONENTS_NOT_FOUND = 403,
ALREADY_DESTROYED_PLATFORM = 404,
ASYNC_INITIALIZERS_STILL_RUNNING = 405,

// Styling Errors

// Declarations Errors

// i18n Errors

// Compilation Errors
// JIT Compilation Errors
}

export class RuntimeError extends Error {
constructor(public code: RuntimeErrorCode, message: string) {
super(formatRuntimeError(code, message));
export class RuntimeError<T = RuntimeErrorCode> extends Error {
constructor(public code: T, message: string) {
super(formatRuntimeError<T>(code, message));
}
}

// Contains a set of error messages that have details guides at angular.io.
// Full list of available error guides can be found at https://angular.io/errors
/* tslint:disable:no-toplevel-property-access */
export const RUNTIME_ERRORS_WITH_GUIDES = new Set([
RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED,
RuntimeErrorCode.CYCLIC_DI_DEPENDENCY,
RuntimeErrorCode.PROVIDER_NOT_FOUND,
RuntimeErrorCode.MULTIPLE_COMPONENTS_MATCH,
RuntimeErrorCode.EXPORT_NOT_FOUND,
RuntimeErrorCode.PIPE_NOT_FOUND,
]);
/* tslint:enable:no-toplevel-property-access */

/** Called to format a runtime error */
export function formatRuntimeError(code: RuntimeErrorCode, message: string): string {
const fullCode = code ? `NG0${code}: ` : '';
export function formatRuntimeError<T = RuntimeErrorCode>(code: T, message: string): string {
const codeAsNumber = code as unknown as number;
// Error code might be a negative number, which is a special marker that instructs the logic to
// generate a link to the error details page on angular.io.
const fullCode = `NG0${Math.abs(codeAsNumber)}`;

let errorMessage = `${fullCode}${message}`;
let errorMessage = `${fullCode}${message ? ': ' + message : ''}`;

// Some runtime errors are still thrown without `ngDevMode` (for example
// `throwProviderNotFoundError`), so we add `ngDevMode` check here to avoid pulling
// `RUNTIME_ERRORS_WITH_GUIDES` symbol into prod bundles.
// TODO: revisit all instances where `RuntimeError` is thrown and see if `ngDevMode` can be added
// there instead to tree-shake more devmode-only code (and eventually remove `ngDevMode` check
// from this code).
if (ngDevMode && RUNTIME_ERRORS_WITH_GUIDES.has(code)) {
errorMessage = `${errorMessage}. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/NG0${code}`;
if (ngDevMode && codeAsNumber < 0) {
errorMessage = `${errorMessage}. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/${fullCode}`;
}
return errorMessage;
}
2 changes: 0 additions & 2 deletions packages/core/src/render3/errors.ts
Expand Up @@ -32,8 +32,6 @@ export function throwErrorIfNoChangesMode(
` It seems like the view has been created after its parent and its children have been dirty checked.` +
` Has it been created in a change detection hook?`;
}
// TODO: include debug context, see `viewDebugError` function in
// `packages/core/src/view/errors.ts` for reference.
throw new RuntimeError(RuntimeErrorCode.EXPRESSION_CHANGED_AFTER_CHECKED, msg);
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/errors_di.ts
Expand Up @@ -7,6 +7,7 @@
*/
import {InjectorType} from '../di/interface/defs';
import {stringify} from '../util/stringify';

import {RuntimeError, RuntimeErrorCode} from './error_code';
import {stringifyForError} from './util/stringify_utils';

Expand Down
32 changes: 32 additions & 0 deletions packages/core/test/runtime_error_spec.ts
@@ -0,0 +1,32 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {RuntimeError, RuntimeErrorCode} from '../src/render3/error_code';

describe('RuntimeError utils', () => {
it('should format the error message correctly', () => {
// Error with a guide, but without an error message.
let errorInstance = new RuntimeError(RuntimeErrorCode.EXPORT_NOT_FOUND, '');
expect(errorInstance.toString())
.toBe('Error: NG0301. Find more at https://angular.io/errors/NG0301');

// Error without a guide and an error message.
errorInstance = new RuntimeError(RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR, '');
expect(errorInstance.toString()).toBe('Error: NG0305');

// Error without a guide, but with an error message.
errorInstance =
new RuntimeError(RuntimeErrorCode.TEMPLATE_STRUCTURE_ERROR, 'Some error message');
expect(errorInstance.toString()).toBe('Error: NG0305: Some error message');

// Error with both a guide and an error message.
errorInstance = new RuntimeError(RuntimeErrorCode.EXPORT_NOT_FOUND, 'Some error message');
expect(errorInstance.toString())
.toBe('Error: NG0301: Some error message. Find more at https://angular.io/errors/NG0301');
});
});

0 comments on commit c4fbd85

Please sign in to comment.