docs: add new debugging and troubleshooting di guide

(cherry picked from commit 13e019a1bb)
This commit is contained in:
Ben Hong 2026-02-20 13:01:11 -05:00 committed by Jessica Janiuk
parent c2cedd1954
commit 390efd51e7
11 changed files with 1250 additions and 13 deletions

View file

@ -341,6 +341,12 @@ export const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'guide/di/di-in-action',
contentPath: 'guide/di/di-in-action',
},
{
label: 'Debugging and troubleshooting DI',
path: 'guide/di/debugging-and-troubleshooting-di',
contentPath: 'guide/di/debugging-and-troubleshooting-di',
status: 'new',
},
],
},
{

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
# Invalid Injection Token
This error occurs when Angular cannot resolve a dependency for a class during dependency injection. This most commonly affects classes using constructor injection, where Angular relies on TypeScript metadata to determine parameter types.
The most common causes are:
1. A service class is missing the `@Injectable()` decorator
2. An `InjectionToken` lacks a proper provider definition
3. A constructor parameter cannot be resolved
NOTE: The `inject()` function takes an explicit token, so the "unresolvable parameter" scenario does not apply to it directly. However, if the injected class itself is missing `@Injectable()` and has its own constructor dependencies, the error can still occur.
## Common scenarios
### Missing `@Injectable()` decorator
When a class has constructor dependencies but lacks the `@Injectable()` decorator, Angular cannot resolve its parameters:
```ts {header: 'Missing @Injectable() decorator'}
export class UserClient {
constructor(private http: HttpClient) {} // Angular can't resolve this
}
```
Add the `@Injectable()` decorator to fix this:
```ts
@Injectable({providedIn: 'root'})
export class UserClient {
constructor(private http: HttpClient) {}
}
```
### Unresolvable constructor parameters
This error also appears when Angular cannot determine the type of a constructor parameter:
```ts
@Injectable({providedIn: 'root'})
export class DataStore {
// Angular can't resolve 'config' without a provider
constructor(private config: AppConfig) {}
}
```
Ensure all constructor parameters either have providers configured or use `@Optional()` for optional dependencies.
## Debugging the error
The error message includes details about which token could not be resolved:
- `Can't resolve all parameters for X: (?, ?, ?)` — The `?` marks indicate unresolvable parameters. Check that the class has `@Injectable()` and all dependencies have providers.
- `Token X is missing a ɵprov definition` — An `InjectionToken` was used without configuring a provider. Register the token with a value using `{provide: TOKEN, useValue: ...}` or add a default factory to the token definition.
Work backwards from the error's stack trace to identify where the problematic injection occurs, then verify that:
1. The class has `@Injectable()` decorator
2. All constructor parameters have registered providers
3. Any `InjectionToken` has a configured provider or default value

View file

@ -0,0 +1,76 @@
# Injector has already been destroyed
This error occurs when you attempt to retrieve a service from an injector that has already been destroyed. This typically happens when code tries to access dependencies after a component, directive, or module has been destroyed.
## Common scenarios
### Accessing services in callbacks after destruction
When a component is destroyed, its injector is also destroyed. If an async callback later tries to access services, this error occurs:
```ts
@Component({
/*...*/
})
export class UserProfile implements OnInit {
private userClient = inject(UserClient);
ngOnInit() {
setTimeout(() => {
// ERROR: If component was destroyed before timeout fires,
// the injector is no longer available
this.userClient.fetchData();
}, 5000);
}
}
```
### Accessing services after unsubscribing
Similar issues occur with observables if cleanup happens in the wrong order:
```ts
@Component({
/*...*/
})
export class DataView implements OnDestroy {
private dataStore = inject(DataStore);
ngOnDestroy() {
// Problematic: attempting to use the injector during destruction
// after other cleanup may have occurred
this.dataStore.cleanup();
}
}
```
## Debugging the error
To fix this error:
1. **Check async operations** — Ensure callbacks, promises, and subscriptions are cancelled when the component is destroyed. Use `takeUntilDestroyed()` or `DestroyRef` for cleanup.
2. **Capture dependencies early** — Store references to services in class fields rather than accessing the injector in callbacks.
3. **Guard against destroyed state** — For operations that might outlive the component, check if the component is still active before accessing services.
```ts
@Component({
/*...*/
})
export class UserProfile implements OnInit {
private destroyRef = inject(DestroyRef);
private userClient = inject(UserClient);
ngOnInit() {
// Use takeUntilDestroyed to automatically cancel when destroyed
interval(5000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userClient.fetchData();
});
}
}
```
The stack trace indicates where the destroyed injector was accessed. Work backwards to identify the async operation that outlived its component.

View file

@ -0,0 +1,75 @@
# EnvironmentProviders in wrong context
This error occurs when `EnvironmentProviders` are used in a context that only accepts regular providers, such as a component's `providers` array. Environment providers are designed for application-wide configuration and can only be used in environment injectors (like the root injector configured in `bootstrapApplication` or route configurations).
## Common scenarios
### Using `provideHttpClient()` in component providers
Functions like `provideHttpClient()` return `EnvironmentProviders`, which cannot be used at the component level:
```ts
@Component({
providers: [
provideHttpClient(), // ERROR: EnvironmentProviders can't be used here
],
})
export class UserProfile {}
```
### Using `importProvidersFrom()` in component providers
The `importProvidersFrom()` function also returns `EnvironmentProviders`:
```ts
@Component({
providers: [
importProvidersFrom(SomeModule), // ERROR: can't be used for component providers
],
})
export class DataView {}
```
## Debugging the error
Move the environment providers to an appropriate location:
### For application-wide providers
Configure environment providers in `bootstrapApplication`:
```ts
bootstrapApplication(App, {
providers: [provideHttpClient(), importProvidersFrom(SomeModule)],
});
```
### For route-specific providers
Use the `providers` array in route configurations:
```ts
const routes: Routes = [
{
path: 'admin',
component: AdminView,
providers: [provideHttpClient(withInterceptors([authInterceptor]))],
},
];
```
### For component-level services
If you need component-scoped services, use regular providers instead of environment providers:
```ts
@Component({
providers: [
UserClient, // Regular provider - this works
{provide: API_URL, useValue: '/api'}, // Value provider - this works
],
})
export class UserProfile {}
```
The error message specifies which provider caused the issue. Check that all items in your component's `providers` array are regular providers, not environment providers returned by functions like `provideHttpClient()`, `provideRouter()`, or `importProvidersFrom()`.

View file

@ -8,6 +8,9 @@
| `NG0200` | [Circular Dependency in DI](errors/NG0200) |
| `NG0201` | [No Provider Found](errors/NG0201) |
| `NG0203` | [`inject()` must be called from an injection context](errors/NG0203) |
| `NG0204` | [Invalid Injection Token](errors/NG0204) |
| `NG0205` | [Injector has already been destroyed](errors/NG0205) |
| `NG0207` | [EnvironmentProviders in wrong context](errors/NG0207) |
| `NG0209` | [Invalid multi provider](errors/NG0209) |
| `NG0300` | [Selector Collision](errors/NG0300) |
| `NG0301` | [Export Not Found](errors/NG0301) |

View file

@ -74,7 +74,7 @@ export const enum RuntimeErrorCode {
// (undocumented)
INFINITE_CHANGE_DETECTION = 103,
// (undocumented)
INJECTOR_ALREADY_DESTROYED = 205,
INJECTOR_ALREADY_DESTROYED = -205,
// (undocumented)
INVALID_APP_ID = 211,
// (undocumented)
@ -90,7 +90,7 @@ export const enum RuntimeErrorCode {
// (undocumented)
INVALID_INHERITANCE = 903,
// (undocumented)
INVALID_INJECTION_TOKEN = 204,
INVALID_INJECTION_TOKEN = -204,
// (undocumented)
INVALID_MULTI_PROVIDER = -209,
// (undocumented)
@ -152,7 +152,7 @@ export const enum RuntimeErrorCode {
// (undocumented)
PROVIDED_BOTH_ZONE_AND_ZONELESS = 408,
// (undocumented)
PROVIDER_IN_WRONG_CONTEXT = 207,
PROVIDER_IN_WRONG_CONTEXT = -207,
// (undocumented)
PROVIDER_NOT_FOUND = -201,
// (undocumented)

View file

@ -37,9 +37,9 @@ export const enum RuntimeErrorCode {
PROVIDER_NOT_FOUND = -201,
INVALID_FACTORY_DEPENDENCY = 202,
MISSING_INJECTION_CONTEXT = -203,
INVALID_INJECTION_TOKEN = 204,
INJECTOR_ALREADY_DESTROYED = 205,
PROVIDER_IN_WRONG_CONTEXT = 207,
INVALID_INJECTION_TOKEN = -204,
INJECTOR_ALREADY_DESTROYED = -205,
PROVIDER_IN_WRONG_CONTEXT = -207,
MISSING_INJECTION_TOKEN = 208,
INVALID_MULTI_PROVIDER = -209,
MISSING_DOCUMENT = 210,

View file

@ -73,7 +73,7 @@ describe('DestroyRef', () => {
expect(() => {
destroyRef.onDestroy(() => {});
}).toThrowError('NG0205: Injector has already been destroyed.');
}).toThrowError(/NG0205: Injector has already been destroyed./);
});
});

View file

@ -15,10 +15,10 @@ import {
ɵɵdefineInjector,
ɵɵinject,
} from '../../src/core';
import {ERROR_DETAILS_PAGE_BASE_URL} from '../../src/error_details_base_url';
import {createInjector} from '../../src/di/create_injector';
import {InternalInjectFlags} from '../../src/di/interface/injector';
import {R3Injector} from '../../src/di/r3_injector';
import {ERROR_DETAILS_PAGE_BASE_URL} from '../../src/error_details_base_url';
describe('InjectorDef-based createInjector()', () => {
class CircularA {
@ -461,14 +461,14 @@ describe('InjectorDef-based createInjector()', () => {
it('does not allow injection after destroy', () => {
(injector as R3Injector).destroy();
expect(() => injector.get(DeepService)).toThrowError(
'NG0205: Injector has already been destroyed.',
/NG0205: Injector has already been destroyed./,
);
});
it('does not allow double destroy', () => {
(injector as R3Injector).destroy();
expect(() => (injector as R3Injector).destroy()).toThrowError(
'NG0205: Injector has already been destroyed.',
/NG0205: Injector has already been destroyed./,
);
});
@ -506,7 +506,7 @@ describe('InjectorDef-based createInjector()', () => {
static ɵinj = ɵɵdefineInjector({providers: [MissingArgumentType]});
}
expect(() => createInjector(ErrorModule).get(MissingArgumentType)).toThrowError(
"NG0204: Can't resolve all parameters for MissingArgumentType: (?).",
/NG0204: Can't resolve all parameters for MissingArgumentType: \(\?\)./,
);
});
});

View file

@ -33,13 +33,13 @@ import {NgModuleType} from '../../src/render3';
import {getNgModuleDef} from '../../src/render3/def_getters';
import {ComponentFixture, inject, TestBed} from '../../testing';
import {ERROR_DETAILS_PAGE_BASE_URL} from '../../src/error_details_base_url';
import {InternalNgModuleRef, NgModuleFactory} from '../../src/linker/ng_module_factory';
import {
clearModulesForTest,
setAllowDuplicateNgModuleIdsForTest,
} from '../../src/linker/ng_module_registration';
import {stringify} from '../../src/util/stringify';
import {ERROR_DETAILS_PAGE_BASE_URL} from '../../src/error_details_base_url';
class Engine {}
@ -542,7 +542,7 @@ describe('NgModule', () => {
it('should throw when no type and not @Inject (class case)', () => {
expect(() => createInjector([NoAnnotations])).toThrowError(
"NG0204: Can't resolve all parameters for NoAnnotations: (?).",
/NG0204: Can't resolve all parameters for NoAnnotations: \(\?\)./,
);
});