The inject() Function
The inject() function is the modern, functional way to retrieve dependencies in Angular. Instead of declaring constructor parameters, you call inject(SomeService) directly inside a field initializer or factory. This unlocks cleaner classes, composable helper functions, and—most importantly—functional route guards, resolvers, and HTTP interceptors that simply could not exist with constructor-only injection. Since Angular 14 it has been stable, and in modern Angular (17–19) it is the recommended default.
Why inject() exists
Classic dependency injection wired everything through the constructor:
@Component({ selector: 'app-profile', standalone: true })
export class ProfileComponent {
constructor(
private http: HttpClient,
private router: Router,
private auth: AuthService,
) {}
}
That works, but it has friction: long parameter lists, no way to reuse the wiring across classes, and you cannot inject inside a plain function. The inject() function solves all three by reading from the current injection context—the ambient injector that Angular activates while constructing a class or running a DI-aware factory.
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
@Component({ selector: 'app-profile', standalone: true })
export class ProfileComponent {
private http = inject(HttpClient);
private router = inject(Router);
private auth = inject(AuthService);
}
Each field is initialized when the instance is created, so the dependencies are available exactly as they would be from the constructor.
Where you can call inject()
inject() only works inside an injection context. Calling it elsewhere throws NG0203. Valid locations include:
| Location | Allowed? | Notes |
|---|---|---|
| Class field initializer | Yes | The most common usage. |
| Constructor body | Yes | Equivalent to a constructor parameter. |
useFactory / provider factory | Yes | Factory runs inside the injector. |
| Functional guard / resolver / interceptor | Yes | Angular runs them in context. |
runInInjectionContext() callback | Yes | Manually establish a context. |
Lifecycle hook (ngOnInit, etc.) | No | Context is gone by then. |
Event handler / setTimeout callback | No | Throws NG0203. |
Gotcha: A common mistake is calling
inject()lazily inside a method or async callback. Resolve dependencies at field-initializer time and store them; the injection context is only active during construction.
Functional guards and interceptors
The biggest payoff is functional, tree-shakable building blocks. A route guard becomes a plain function:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { redirect: state.url },
});
};
An HTTP interceptor is equally compact and can pull in any service it needs:
import { inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq);
};
You register them where they are needed, with no class boilerplate:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authGuard } from './app/auth.guard';
import { authInterceptor } from './app/auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideRouter([
{ path: 'dashboard', canActivate: [authGuard], loadComponent: () => import('./app/dashboard.component') },
]),
provideHttpClient(withInterceptors([authInterceptor])),
],
});
Options: optional, self, host, skipSelf
inject() accepts a second argument of flags that mirror the legacy parameter decorators. This lets you express optional or scoped lookups inline.
import { inject, InjectionToken } from '@angular/core';
const API_URL = new InjectionToken<string>('API_URL');
@Injectable({ providedIn: 'root' })
export class ConfigService {
// Returns null instead of throwing if no provider exists.
private apiUrl = inject(API_URL, { optional: true }) ?? 'https://api.example.com';
getUrl(): string {
return this.apiUrl;
}
}
| Option | Effect |
|---|---|
optional: true | Returns null instead of throwing when not found. |
self: true | Only look in the current injector. |
skipSelf: true | Skip the current injector, start at the parent. |
host: true | Stop searching at the host element’s injector. |
Composable injection logic
Because inject() works inside any function called from a context, you can extract reusable “inject helpers”—the functional equivalent of a base class:
import { inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
export function autoUnsubscribe<T>(source$: Observable<T>) {
const destroyRef = inject(DestroyRef);
return source$.pipe(takeUntilDestroyed(destroyRef));
}
@Component({ selector: 'app-ticker', standalone: true, template: `{{ value }}` })
export class TickerComponent {
value = 0;
constructor() {
autoUnsubscribe(interval(1000)).subscribe(() => this.value++);
}
}
Output:
1 // after 1s
2 // after 2s
3 // after 3s (auto-unsubscribes when the component is destroyed)
Running outside a context
When you genuinely need DI outside construction—for example deep inside an async pipeline—capture an EnvironmentInjector and use runInInjectionContext():
import { inject, EnvironmentInjector, runInInjectionContext } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ReportService {
private injector = inject(EnvironmentInjector);
async generate() {
const data = await fetch('/api/report').then((r) => r.json());
runInInjectionContext(this.injector, () => {
const http = inject(HttpClient); // valid again here
http.post('/api/audit', data).subscribe();
});
}
}
Best Practices
- Prefer
inject()over constructor parameters for new code—it reads cleaner and works in functions, not just classes. - Call
inject()at field-initializer time and store the result; never defer it to a method,setTimeout, or event handler. - Use functional guards, resolvers, and interceptors instead of class-based ones; they are tree-shakable and far less boilerplate.
- Reach for
{ optional: true }rather than wrappinginject()in atry/catchwhen a dependency may be absent. - Extract shared injection logic into small helper functions to compose behavior without inheritance.
- When you must inject outside construction, use
runInInjectionContext()with a capturedEnvironmentInjector—don’t fightNG0203with hacks.