Skip to content
Angular ng services 5 min read

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:

LocationAllowed?Notes
Class field initializerYesThe most common usage.
Constructor bodyYesEquivalent to a constructor parameter.
useFactory / provider factoryYesFactory runs inside the injector.
Functional guard / resolver / interceptorYesAngular runs them in context.
runInInjectionContext() callbackYesManually establish a context.
Lifecycle hook (ngOnInit, etc.)NoContext is gone by then.
Event handler / setTimeout callbackNoThrows 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;
  }
}
OptionEffect
optional: trueReturns null instead of throwing when not found.
self: trueOnly look in the current injector.
skipSelf: trueSkip the current injector, start at the parent.
host: trueStop 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 wrapping inject() in a try/catch when 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 captured EnvironmentInjector—don’t fight NG0203 with hacks.
Last updated June 14, 2026
Was this helpful?