Understanding Angular Injection Context
Angular introduced the inject()
function in recent versions, which allows us to obtain a reference to a provider in a functional way rather than using the Injector.get()
method. However, if you have used it or a library that uses it under the hood, you may have encountered the following error:
inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`.
An injection context is simply a term for “someone is using the inject()
function but there is no injector
available in the current stack frame."
Angular has two global variables that can hold an injector
at a certain point in time: one for an Injector
and one for NodeInjector
. Here are the code snippets for both:
let _currentInjector = undefined;export function getCurrentInjector() { return _currentInjector;}export function setCurrentInjector(injector: Injector|undefined|null { const former = _currentInjector; _currentInjector = injector; return former;}
let _injectImplementationexport function getInjectImplementation() { return _injectImplementation;}
In Angular v16, a new function named assertInInjectionContext
was introduced that checks if the current stack frame is running inside an injection
context:
export function assertInInjectionContext(debugFn: Function): void { if (!getInjectImplementation() && !getCurrentInjector()) { throw new RuntimeError( RuntimeErrorCode.MISSING_INJECTION_CONTEXT, ngDevMode && (debugFn.name + '() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`')); }}
If no injector
is available, it throws the error. We can use this function if we have code that relies on the inject()
function and we want to verify that the consumer is using it where an injector
is available. For example:
import { ElementRef, assertInInjectionContext, inject,} from '@angular/core';export function injectNativeElement<T extends Element>(): T { assertInInjectionContext(injectNativeElement); return inject(ElementRef).nativeElement;}
Let’s go over the phases in which we can use the inject()
function:
- In the
factory
function specified for anInjectionToken
:
export const FOO = new InjectionToken('FOO', { factory() { const value = inject(SOME_PROVIDER); return ... },});
- In the
factory
function specified foruseFactory
of aProvider
or an@Injectable
.
@Component({ providers: [ { provide: FOO, useFactory() { const value = inject(SOME_PROVIDER); return ... }, }, ]})export class FooComponent {}
- During the construction of a class that is being instantiated by the DI system, such as an
@Injectable
or@Component
, via the constructor or in the initializer for fields of such classes:
@Component({})export class FooComponent { foo = inject(FOO); constructor(private ngZone: NgZone = inject(NgZone)) { const bar = inject(BAR); }}
We can see in the source code that Angular calls the getNodeInjectable
function right before instantiating the component and sets the current injector, which in this case, is the component node injector.
- Inside a function that is executed using the
runInInjectionContext
function:
import { runInInjectionContext } from '@angular/core';export class FooComponent { ngOnInit() { runInInjectionContext(this.injector, () => { console.log( 'I can access the NodeInjector using inject()', inject(ElementRef) ); })}
By examining the source code of this function, we can determine that it assigns the passed injector
to the global injector variable that we previously encountered.
It’s worth noting that the inject
function can only be used synchronously, and isn’t compatible with asynchronous callbacks or any await points.
Follow me on Medium or Twitter to read more about Angular and JS!