Skip to content

Commit 728fe69

Browse files
ocombematsko
authored andcommitted
feat(ivy): improve stacktrace for R3Injector errors (angular#28207)
Improve the stacktrace for `R3Injector` errors by adding the source component (or module) that tried to inject the missing provider, as well as the name of the injector which triggered the error (`R3Injector`). e.g.: ``` R3InjectorError(SomeModule)[car -> SportsCar]: NullInjectorError: No provider for SportsCar! ``` FW-807 #resolve FW-875 #resolve PR Close angular#28207
1 parent 7219639 commit 728fe69

File tree

10 files changed

+232
-79
lines changed

10 files changed

+232
-79
lines changed

packages/core/src/di/injector.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import {Type} from '../interface/type';
1010
import {getClosureSafeProperty} from '../util/property';
1111
import {stringify} from '../util/stringify';
12-
1312
import {resolveForwardRef} from './forward_ref';
1413
import {InjectionToken} from './injection_token';
1514
import {inject} from './injector_compatibility';
@@ -42,7 +41,9 @@ export class NullInjector implements Injector {
4241
// reason why correctly written application should cause this exception.
4342
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
4443
// if(ngDevMode) debugger;
45-
throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
44+
const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
45+
error.name = 'NullInjectorError';
46+
throw error;
4647
}
4748
return notFoundValue;
4849
}
@@ -131,7 +132,7 @@ const MULTI_PROVIDER_FN = function(): any[] {
131132
export const USE_VALUE =
132133
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
133134
const NG_TOKEN_PATH = 'ngTokenPath';
134-
const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
135+
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
135136
const enum OptionFlags {
136137
Optional = 1 << 0,
137138
CheckSelf = 1 << 1,
@@ -167,14 +168,7 @@ export class StaticInjector implements Injector {
167168
try {
168169
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
169170
} catch (e) {
170-
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
171-
if (token[SOURCE]) {
172-
tokenPath.unshift(token[SOURCE]);
173-
}
174-
e.message = formatError('\n' + e.message, tokenPath, this.source);
175-
e[NG_TOKEN_PATH] = tokenPath;
176-
e[NG_TEMP_TOKEN_PATH] = null;
177-
throw e;
171+
return catchInjectorError(e, token, 'StaticInjectorError', this.source);
178172
}
179173
}
180174

@@ -200,8 +194,6 @@ interface DependencyRecord {
200194
options: number;
201195
}
202196

203-
type TokenPath = Array<any>;
204-
205197
function resolveProvider(provider: SupportedProvider): Record {
206198
const deps = computeDeps(provider);
207199
let fn: Function = IDENT;
@@ -385,7 +377,20 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
385377
return deps;
386378
}
387379

388-
function formatError(text: string, obj: any, source: string | null = null): string {
380+
export function catchInjectorError(
381+
e: any, token: any, injectorErrorName: string, source: string | null): never {
382+
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
383+
if (token[SOURCE]) {
384+
tokenPath.unshift(token[SOURCE]);
385+
}
386+
e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source);
387+
e[NG_TOKEN_PATH] = tokenPath;
388+
e[NG_TEMP_TOKEN_PATH] = null;
389+
throw e;
390+
}
391+
392+
function formatError(
393+
text: string, obj: any, injectorErrorName: string, source: string | null = null): string {
389394
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
390395
let context = stringify(obj);
391396
if (obj instanceof Array) {
@@ -401,9 +406,9 @@ function formatError(text: string, obj: any, source: string | null = null): stri
401406
}
402407
context = `{${parts.join(', ')}}`;
403408
}
404-
return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
409+
return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
405410
}
406411

407412
function staticError(text: string, obj: any): Error {
408-
return new Error(formatError(text, obj));
413+
return new Error(formatError(text, obj, 'StaticInjectorError'));
409414
}

packages/core/src/di/interface/injector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
export enum InjectFlags {
1616
// TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files.
1717

18+
/** Check self and check parent injector if needed */
1819
Default = 0b0000,
19-
2020
/**
2121
* Specifies that an injector should retrieve a dependency from any injector until reaching the
2222
* host element of the current component. (Only used with Element Injector)

packages/core/src/di/r3_injector.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@
99
import {OnDestroy} from '../interface/lifecycle_hooks';
1010
import {Type} from '../interface/type';
1111
import {stringify} from '../util/stringify';
12-
1312
import {resolveForwardRef} from './forward_ref';
1413
import {InjectionToken} from './injection_token';
15-
import {INJECTOR, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE} from './injector';
14+
import {INJECTOR, Injector, NG_TEMP_TOKEN_PATH, NullInjector, USE_VALUE, catchInjectorError} from './injector';
1615
import {inject, injectArgs, setCurrentInjector} from './injector_compatibility';
1716
import {InjectableDef, InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef} from './interface/defs';
1817
import {InjectFlags} from './interface/injector';
1918
import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './interface/provider';
2019
import {APP_ROOT} from './scope';
2120

2221

23-
2422
/**
2523
* Internal type for a single provider in a deep provider array.
2624
*/
@@ -72,9 +70,9 @@ interface Record<T> {
7270
*/
7371
export function createInjector(
7472
defType: /* InjectorType<any> */ any, parent: Injector | null = null,
75-
additionalProviders: StaticProvider[] | null = null): Injector {
73+
additionalProviders: StaticProvider[] | null = null, name?: string): Injector {
7674
parent = parent || getNullInjector();
77-
return new R3Injector(defType, additionalProviders, parent);
75+
return new R3Injector(defType, additionalProviders, parent, name);
7876
}
7977

8078
export class R3Injector {
@@ -99,15 +97,17 @@ export class R3Injector {
9997
*/
10098
private readonly isRootInjector: boolean;
10199

100+
readonly source: string|null;
101+
102102
/**
103103
* Flag indicating that this injector was previously destroyed.
104104
*/
105105
get destroyed(): boolean { return this._destroyed; }
106106
private _destroyed = false;
107107

108108
constructor(
109-
def: InjectorType<any>, additionalProviders: StaticProvider[]|null,
110-
readonly parent: Injector) {
109+
def: InjectorType<any>, additionalProviders: StaticProvider[]|null, readonly parent: Injector,
110+
source: string|null = null) {
111111
// Start off by creating Records for every provider declared in every InjectorType
112112
// included transitively in `def`.
113113
const dedupStack: InjectorType<any>[] = [];
@@ -127,6 +127,9 @@ export class R3Injector {
127127

128128
// Eagerly instantiate the InjectorType classes themselves.
129129
this.injectorDefTypes.forEach(defType => this.get(defType));
130+
131+
// Source name, used for debugging
132+
this.source = source || (def instanceof Array ? null : stringify(def));
130133
}
131134

132135
/**
@@ -152,7 +155,7 @@ export class R3Injector {
152155
}
153156

154157
get<T>(
155-
token: Type<T>|InjectionToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
158+
token: Type<T>|InjectionToken<T>, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
156159
flags = InjectFlags.Default): T {
157160
this.assertNotDestroyed();
158161
// Set the injection context.
@@ -182,7 +185,21 @@ export class R3Injector {
182185
// Select the next injector based on the Self flag - if self is set, the next injector is
183186
// the NullInjector, otherwise it's the parent.
184187
const nextInjector = !(flags & InjectFlags.Self) ? this.parent : getNullInjector();
185-
return nextInjector.get(token, notFoundValue);
188+
return nextInjector.get(token, flags & InjectFlags.Optional ? null : notFoundValue);
189+
} catch (e) {
190+
if (e.name === 'NullInjectorError') {
191+
const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || [];
192+
path.unshift(stringify(token));
193+
if (previousInjector) {
194+
// We still have a parent injector, keep throwing
195+
throw e;
196+
} else {
197+
// Format & throw the final error message when we don't have any previous injector
198+
return catchInjectorError(e, token, 'R3InjectorError', this.source);
199+
}
200+
} else {
201+
throw e;
202+
}
186203
} finally {
187204
// Lastly, clean up the state by restoring the previous injector.
188205
setCurrentInjector(previousInjector);

packages/core/src/render3/ng_module_ref.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo
1616
import {NgModuleDef} from '../metadata/ng_module';
1717
import {assertDefined} from '../util/assert';
1818
import {stringify} from '../util/stringify';
19-
2019
import {ComponentFactoryResolver} from './component_ref';
2120
import {getNgModuleDef} from './definition';
2221

@@ -52,7 +51,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
5251
},
5352
COMPONENT_FACTORY_RESOLVER
5453
];
55-
this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders) as R3Injector;
54+
this._r3Injector = createInjector(
55+
ngModuleType, _parent, additionalProviders, stringify(ngModuleType)) as R3Injector;
5656
this.instance = this.get(ngModuleType);
5757
}
5858

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55
{
66
"name": "CIRCULAR"
77
},
8+
{
9+
"name": "CIRCULAR"
10+
},
11+
{
12+
"name": "EMPTY"
13+
},
814
{
915
"name": "EMPTY_ARRAY"
1016
},
1117
{
1218
"name": "EmptyErrorImpl"
1319
},
20+
{
21+
"name": "IDENT"
22+
},
1423
{
1524
"name": "INJECTOR"
1625
},
@@ -23,15 +32,36 @@
2332
{
2433
"name": "InjectionToken"
2534
},
35+
{
36+
"name": "Injector"
37+
},
38+
{
39+
"name": "MULTI_PROVIDER_FN"
40+
},
41+
{
42+
"name": "NEW_LINE"
43+
},
2644
{
2745
"name": "NG_INJECTABLE_DEF"
2846
},
2947
{
3048
"name": "NG_INJECTOR_DEF"
3149
},
50+
{
51+
"name": "NG_TEMP_TOKEN_PATH"
52+
},
53+
{
54+
"name": "NG_TOKEN_PATH"
55+
},
3256
{
3357
"name": "NOT_YET"
3458
},
59+
{
60+
"name": "NO_NEW_LINE"
61+
},
62+
{
63+
"name": "NULL_INJECTOR"
64+
},
3565
{
3666
"name": "NULL_INJECTOR"
3767
},
@@ -50,6 +80,9 @@
5080
{
5181
"name": "R3Injector"
5282
},
83+
{
84+
"name": "SOURCE"
85+
},
5386
{
5487
"name": "ScopedService"
5588
},
@@ -60,7 +93,7 @@
6093
"name": "SkipSelf"
6194
},
6295
{
63-
"name": "THROW_IF_NOT_FOUND"
96+
"name": "StaticInjector"
6497
},
6598
{
6699
"name": "USE_VALUE"
@@ -83,6 +116,12 @@
83116
{
84117
"name": "_currentInjector"
85118
},
119+
{
120+
"name": "catchInjectorError"
121+
},
122+
{
123+
"name": "computeDeps"
124+
},
86125
{
87126
"name": "couldBeInjectableType"
88127
},
@@ -98,6 +137,9 @@
98137
{
99138
"name": "defineInjector"
100139
},
140+
{
141+
"name": "formatError"
142+
},
101143
{
102144
"name": "forwardRef"
103145
},
@@ -155,19 +197,37 @@
155197
{
156198
"name": "makeRecord"
157199
},
200+
{
201+
"name": "multiProviderMixError"
202+
},
158203
{
159204
"name": "providerToFactory"
160205
},
161206
{
162207
"name": "providerToRecord"
163208
},
209+
{
210+
"name": "recursivelyProcessProviders"
211+
},
164212
{
165213
"name": "resolveForwardRef"
166214
},
215+
{
216+
"name": "resolveProvider"
217+
},
218+
{
219+
"name": "resolveToken"
220+
},
167221
{
168222
"name": "setCurrentInjector"
169223
},
224+
{
225+
"name": "staticError"
226+
},
170227
{
171228
"name": "stringify"
229+
},
230+
{
231+
"name": "tryResolveToken"
172232
}
173233
]

0 commit comments

Comments
 (0)