Skip to content

Commit

Permalink
refactor(core): the RuntimeError class should support more compact …
Browse files Browse the repository at this point in the history
…syntax (angular#44783)

This commit refactors the `RuntimeError` class to support a short version of providing error messages:
```
throw new RuntimeError(
  RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
  ngDevMode && 'Injector has already been destroyed.');
```
In prod mode, the second argument becomes `false` andn this commit extends the typings to support that.

This commit also contains a couple places were the `RuntimeError` class is used to demostrate the compact form.

PR Close angular#44783
  • Loading branch information
AndrewKushnir authored and thePunderWoman committed Feb 1, 2022
1 parent db05ae1 commit ed1732c
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 27 deletions.
8 changes: 4 additions & 4 deletions goldens/public-api/core/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
```ts

// @public
export function formatRuntimeError<T = RuntimeErrorCode>(code: T, message: string): string;
export function formatRuntimeError<T extends number = RuntimeErrorCode>(code: T, message: null | false | string): string;

// @public (undocumented)
export class RuntimeError<T = RuntimeErrorCode> extends Error {
constructor(code: T, message: string);
// @public
export class RuntimeError<T extends number = RuntimeErrorCode> extends Error {
constructor(code: T, message: null | false | string);
// (undocumented)
code: T;
}
Expand Down
24 changes: 10 additions & 14 deletions packages/core/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,9 @@ export class R3Injector {

private assertNotDestroyed(): void {
if (this._destroyed) {
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ?
'Injector has already been destroyed.' :
'';
throw new RuntimeError(RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED, errorMessage);
throw new RuntimeError(
RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
ngDevMode && 'Injector has already been destroyed.');
}
}

Expand Down Expand Up @@ -450,10 +449,9 @@ function injectableDefOrInjectorDefFactory(token: ProviderToken<any>): FactoryFn
// InjectionTokens should have an injectable def (ɵprov) and thus should be handled above.
// If it's missing that, it's an error.
if (token instanceof InjectionToken) {
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ?
`Token ${stringify(token)} is missing a ɵprov definition.` :
'';
throw new RuntimeError(RuntimeErrorCode.INVALID_INJECTION_TOKEN, errorMessage);
throw new RuntimeError(
RuntimeErrorCode.INVALID_INJECTION_TOKEN,
ngDevMode && `Token ${stringify(token)} is missing a ɵprov definition.`);
}

// Undecorated types can sometimes be created if they have no constructor arguments.
Expand All @@ -462,19 +460,17 @@ function injectableDefOrInjectorDefFactory(token: ProviderToken<any>): FactoryFn
}

// There was no way to resolve a factory for this token.
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ? 'unreachable' : '';
throw new RuntimeError(RuntimeErrorCode.INVALID_INJECTION_TOKEN, errorMessage);
throw new RuntimeError(RuntimeErrorCode.INVALID_INJECTION_TOKEN, ngDevMode && 'unreachable');
}

function getUndecoratedInjectableFactory(token: Function) {
// If the token has parameters then it has dependencies that we cannot resolve implicitly.
const paramLength = token.length;
if (paramLength > 0) {
const args: string[] = newArray(paramLength, '?');
const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ?
`Can't resolve all parameters for ${stringify(token)}: (${args.join(', ')}).` :
'';
throw new RuntimeError(RuntimeErrorCode.INVALID_INJECTION_TOKEN, errorMessage);
throw new RuntimeError(
RuntimeErrorCode.INVALID_INJECTION_TOKEN,
ngDevMode && `Can't resolve all parameters for ${stringify(token)}: (${args.join(', ')}).`);
}

// The constructor function appears to have no parameters.
Expand Down
32 changes: 25 additions & 7 deletions packages/core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,40 @@ export const enum RuntimeErrorCode {
UNSAFE_VALUE_IN_SCRIPT = 905
}

export class RuntimeError<T = RuntimeErrorCode> extends Error {
constructor(public code: T, message: string) {
/**
* Class that represents a runtime error.
* Formats and outputs the error message in a consistent way.
*
* Example:
* ```
* throw new RuntimeError(
* RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
* ngDevMode && 'Injector has already been destroyed.');
* ```
*
* Note: the `message` argument contains a descriptive error message as a string in development
* mode (when the `ngDevMode` is defined). In production mode (after tree-shaking pass), the
* `message` argument becomes `false`, thus we account for it in the typings and the runtime logic.
*/
export class RuntimeError<T extends number = RuntimeErrorCode> extends Error {
constructor(public code: T, message: null|false|string) {
super(formatRuntimeError<T>(code, message));
}
}

/** Called to format a runtime error */
export function formatRuntimeError<T = RuntimeErrorCode>(code: T, message: string): string {
const codeAsNumber = code as unknown as number;
/**
* Called to format a runtime error.
* See additional info on the `message` argument type in the `RuntimeError` class description.
*/
export function formatRuntimeError<T extends number = RuntimeErrorCode>(
code: T, message: null|false|string): string {
// 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)}`;
const fullCode = `NG0${Math.abs(code)}`;

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

if (ngDevMode && codeAsNumber < 0) {
if (ngDevMode && code < 0) {
errorMessage = `${errorMessage}. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/${fullCode}`;
}
return errorMessage;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,8 @@ function cacheMatchingLocalNames(
const index = exportsMap[localRefs[i + 1]];
if (index == null)
throw new RuntimeError(
RuntimeErrorCode.EXPORT_NOT_FOUND, `Export of name '${localRefs[i + 1]}' not found!`);
RuntimeErrorCode.EXPORT_NOT_FOUND,
ngDevMode && `Export of name '${localRefs[i + 1]}' not found!`);
localNames.push(localRefs[i], index);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/runtime_error_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {RuntimeError, RuntimeErrorCode} from '../src/errors';
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, '');
let errorInstance = new RuntimeError<RuntimeErrorCode>(RuntimeErrorCode.EXPORT_NOT_FOUND, '');
expect(errorInstance.toString())
.toBe('Error: NG0301. Find more at https://angular.io/errors/NG0301');

Expand Down

0 comments on commit ed1732c

Please sign in to comment.