inject() Function, runInInjectionContext(), and DI Hierarchy
- Last updated
- Prerequisites:
- Angular dependency injection basics (providers, tokens)
- Understanding of constructor-based DI
- angular
- dependency-injection
- inject
- injection-context
- hierarchy
- di
Read at your depth
The practical view
inject(SomeToken) returns a dependency from the current injection context. Use it in component/pipe/directive constructors, in field initializers, in providers' factory functions, and in guard/resolver functions. const api = inject(ApiService);. It replaces constructor parameter injection for most cases and works in functions too. If you call inject() outside an injection context (e.g. a random utility function called from an event handler), it throws NG0203. To inject in a context-less function, wrap with runInInjectionContext(environmentInjector, () => inject(...)).
The same idea in other frameworks
React Context via useContext is similar; Angular's inject resolves through the injector hierarchy.inject() + runInInjectionContext() — the current topic.inject() in Vue's Composition API mirrors the same hierarchical lookup.getContext()/setContext() in Svelte are the analogue for component-hierarchy injection.Legacy vs modern
Constructor injection vs inject() in a functional guard
Constructor DI requires class scaffolding; inject() works in plain functions (guards, resolvers, factories) with the same hierarchy resolution.
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService) {} // ❌ OLD: requires a class + constructor
canActivate() { return this.auth.isLoggedIn(); }
}export const authGuard: CanActivateFn = () => inject(AuthService).isLoggedIn(); // ✅ NEWInterview gotchas
Context
This tests the ambient-injection-context model — interviewers want to know exactly when DI is available and the standard escape hatches when it isn't.
The mechanical answer
inject() requires an active injection context, which exists while Angular is: constructing components/directives/pipes, executing injectable constructors, running provider factory functions, executing route guards/resolvers/interceptors, and within runInInjectionContext. Calling inject() from an ordinary function invoked later (e.g. an event handler, setTimeout callback, or a utility called from business logic) finds no ambient injector and throws NG0203. Fixes: capture the injected value in an injection-context scope (a component constructor/field initializer) and close over it, or wrap the call site with runInInjectionContext(environmentInjector, () => inject(...)). This is a common migration issue when refactoring class services into functions.
Trap
The naive answer is 'inject() works anywhere because it's global'. It relies on an ambient injector that only exists during DI-driven execution. Another trap: assuming arrow functions in the component body have injection context — they run later (events, timers), so inject() inside them throws. Candidates who describe the ambient injector and runInInjectionContext escape hatch demonstrate real understanding.
Context
This probes the DI resolution mechanics — the injector chain traversal and how element vs environment injectors differ — a core Angular interview area.
The mechanical answer
inject() uses the currently-active injector, then walks the hierarchy: for a component, it starts at the ElementInjector chain (component and ancestor component injectors, which can provide per-element services), then falls through to the ModuleInjector (the @NgModule injector or environment injector), then to the NullInjector, which throws if nothing provides the token. Tokens resolve by matching the provided key (class, InjectionToken, or string) and consulting each injector's records; providers with providedIn: 'root' live in the environment injector, while component-level providers live in the element chain. The resolution caches instances per injector. Tree-shakable tokens (providedIn) are created lazily on first injection.
Trap
The naive answer is 'inject just returns the singleton'. Hierarchy matters: a component-provided service shadows the root one for that subtree, and inject() respects the current element chain, not a global registry. Another trap: thinking providedIn: 'root' bypasses element-level overrides — element providers take precedence for that subtree. Candidates who can describe the Element → Module → Null walk and shadowing rules show real DI mastery.