03 Sep 2025
8 min

Why is inject() better than constructor?

The inject() function was introduced with Angular 14, as an alternative to declaration of dependencies via providers property and via passing them into the constructor. Nowadays it is widely preferred to use inject() instead of constructor. In this article we will discover the benefits that support the argument that using inject() is better and why you should use it.

In this article we will explore how using inject() compares to using constructor in various scenarios. But before that, there will be a runthrough on dependency injection and Angular injection context, and some rules on injection.

Summary: Dependency injection (DI)

Dependency injection, also referred to as DI, is a widely used design pattern that implements Inversion of Control principle. Angular has a powerful built-in DI mechanism that allows creation and delivery of application parts to other parts of the application that need them. With the help of this mechanism you can use dependencies in your application in a flexible way. 

In the DI system there is a dependency consumer and dependency provider

Dependency providers can be of various types, most commonly being the class with @Injectable decorator with providedIn set. For example:

@Injectable({
  providedIn: 'root'
})
class HeroService {}

Setting ‘root’ for providedIn makes the class a singleton and registers it in the application’s root injector. It can also be done like:

@Injectable()
class HeroService {}
// ...
@Component({
  selector: 'app-example',
  template: '...',
  providers: [HeroService] 
})
export class ExampleComponent {
  private _heroService = inject(HeroService); 
}

which creates a scoped instance rather than a singleton, which has to be added manually to the providers array of a component you want to use it in. 

In practice, the provided dependencies are usually services and custom injectables.

There’s an abstraction called Injector which checks its registry if an instance already exists upon request of a dependency, and if not creates and registers a new instance.

The injectors are created during the application bootstrap process so there’s no need to create them manually. More information on dependency providers can be found here.

Injection context

DI relies on a runtime context called injection context. When dependency consumers want to inject services, directives, pipes, other custom injectables etc. they can only do it within the injection context. The rules of DI state that using any injection form has to be within the injection context for the program to work, otherwise an error will occur. Here is an example to demonstrate the meaning:

class MyComponent  {
  private _service1: Service1;
  private _service2: Service2 = inject(Service2); // In context
  private _service3: Service3;
  constructor(private _service4: Service4) { // In context
    this._service1 = inject(Service1) // In context
  }

  data = getData(); // In context

  onSubmit() {
    this._service3 = inject(Service3) // Out of context

    this._service1.method() // Still allowed
  }
}

export function getData(): HttpClient {
  return inject(HttpClient);
}

The situations in which injection context is available are as follows:

  • Construction block of a class with @Injectable or @Component including constructor() and the initializer fields of these classes (see ex. above)
  • Factory function specified for useFactory or a Provider or an @Injectable
  • factory function specified for an InjectionToken
  • Inside a stack frame that runs in an injection context

You can find more detailed information about the injection context here.

Injecting a service outside the inject context will result in Angular throwing NG0203.

Let’s inspect the following example:
main.ts

import { Component, OnInit, inject, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';
import { TodoService } from './todo.service';
import { User, UserService } from './user.service';
import 'zone.js';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h2>ToDos</h2>
    <select (change)="onSelected($event)">
      <option value="">--Select a user--</option>
      @for(user of users(); track user.id) {
        <option [value]=user.id>{{ user.name }}</option>
      }
    </select>
    <button (click)='onClick()'>New Item</button>

    @for(todo of todosForUser(); track todo.id) {
      <div>* {{ todo.title }}</div>
    }
  `,
})
export class App implements OnInit {
  private readonly _todoService = inject(TodoService);
  protected todosForUser = this.todoService.todosForDisplay;

  // private readonly _userService = inject(UserService);
  // users = this._userService.users;
  users = signal<User[]>([]).asReadonly();

  // constructor(private _userService: UserService, 
  //             private _todoService: TodoService) {}

  ngOnInit() {
    const userService = inject(UserService);
    this.users = userService.users;
  }
  onClick() {
    this._todoService.addNewItem();
  }
  onSelected(event: Event) {
    const selectedVal = (event.target as HTMLSelectElement).value;
    if (typeof selectedVal !== 'string') return;
    this._todoService.getTodosForUser(selectedVal);
  }

}
bootstrapApplication(App, appConfig);

The above example injects the userService inside OnInit lifecycle hook with the purpose of initializing. It won’t break the code, but the logic fails because injection works during the construction phase which is in the injection context and not after the construction phase is complete. Also, you will get the following error on your console:

When it comes to your app, as logic fails, you won’t see any users in your list:

The above example is from StackBlitz which you can find in this link and play around 🙂 

How does inject() compare to constructor?

DI via constructor is the traditional method for injecting dependencies and is still supported by the latest Angular versions to this date. With Angular 14 the inject() function was introduced which injects a token from the active Injector. Just like other methods of DI it’s supported only within the injection context. Technically constructor can’t fall out of injection context as a pose to inject(), however, compared to constructor, inject() offers a more powerful and often simpler way to handle dependencies, along with extended possibilities. Nowadays it’s a more popular method than constructor. This chapter will demonstrate the difference between these two methods.

Injecting into standalone functions

Standalone functions refer to external functions which you can use anywhere inside a class.

Let’s first revise the constructor method of DI:

export class App implements OnInit {
// ...
  constructor(private _userService: UserService, private _todoService: TodoService) {}

  ngOnInit() {/* ... */}

// ...
}

The way demonstrated above is a shorthand way of injection during the construction phase. When using constructor as an injection method, you’re only tied to the construction phase of the class, as functions don’t have constructors. Which brings me to say that injection inside standalone functions is possible. If you call a function with injects a service, whether it’s in the injection context or not depends on where you call the function. Let’s take this example:

user.service.ts

import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";

export interface User {
  id: string;
  name: string;
  username: string;
  email: string;
  website: string;
}

export function getUsers() {
  const userUrl = "https://jsonplaceholder.typicode.com/users";
  const http = inject(HttpClient);

  return toSignal(http.get<User[]>(userUrl), {initialValue:[] });
}
 
@Injectable({
    providedIn: 'root'
})
export class UserService {
  // private _userUrl = "https://jsonplaceholder.typicode.com/users";
  // private readonly _http = inject(HttpClient); -> injection context
  // constructor(private _http: HttpClient) { } -> injection context
  // users = toSignal(this._http.get<User[]>(this._userUrl), {initialValue:[] });

  // injection context	
   users = getUsers();
}

As you can see, the getUsers() function serves a role in setting up the variables in the UserService class’ construction phase. Thus, calling inject(HttpClient) is also done within the injection context. However, this method ties you to using inject(), as constructor based injection is not possible. On top of being able to call inject() in various places inside the class construction block it also grants the possibility to handle some construction tasks from outside the class via exported functions.

Inheritance

Another case where inject() function simplifies DI is when dealing with inheritance. We are back to classes, so now I will explain what you can not worry about thanks to inject(). Let’s take an example of a BaseComponent class, and a ChildComponent class that extends BaseComponent:

// base.component.ts
import { OnInit } from '@angular/core';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';

export abstract class BaseComponent implements OnInit {
  constructor(
    protected logger: LoggerService,
    protected errorHandler: ErrorHandlerService
  ) {}

  ngOnInit() {
    this.logger.log('BaseComponent Initialized');
  }
}

BaseComponent class injects LoggerService and ErrorHandlerService through its constructor.

// child.component.ts
import { Component } from '@angular/core';
import { BaseComponent } from './base.component';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';
import { AnalyticsService } from './analytics.service';

@Component({
  selector: 'app-child',
  template: `...`
})
export class ChildComponent extends BaseComponent {
  constructor(
    protected override logger: LoggerService,
    protected override errorHandler: ErrorHandlerService,
    private analytics: AnalyticsService
  ) {
    super(logger, errorHandler);
  }

  trackEvent() {
    this.analytics.track('Child Event');
    this.logger.log('Event tracked from ChildComponent');
  }
}

ChildComponent class injects AnalyticsService class for itself. But notice that its constructor also takes the services which BaseComponent injects to fetch them by calling super(), in the correct order in the base’s constructor. This method looks painful to implement for even bigger projects. 

Now let’s get on to the version where this is replaced by inject() function:

// base.component.ts
import { OnInit, inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';
export abstract class BaseComponent implements OnInit {
  protected logger = inject(LoggerService);
  protected errorHandler = inject(ErrorHandlerService);

  constructor() {}
  ngOnInit() {
    this.logger.log('BaseComponent Initialized');
  }
}

BaseComponent class constructor doesn’t take any arguments. At this point, constructor() block can even be omitted for cleaner code.

// child.component.ts
import { Component, inject } from '@angular/core';
import { BaseComponent } from './base.component';
import { AnalyticsService } from './analytics.service';

@Component({
  selector: 'app-child',
  template: `...`
})
export class ChildComponent extends BaseComponent {
  private analytics = inject(AnalyticsService);

  constructor() {
    super();
  }

  trackEvent() {
    this.analytics.track('Child Event');
    this.logger.log('Event tracked from ChildComponent');
  }
}

ChildComponent injects what it needs. It already extends the BaseComponent class so constructor-wise you’re left with much less work. You don’t have to worry about the order of properties, as calling an empty super() will suffice (it’s JavaScript that requires this but hey 😄).

Conditional injection

Conditional injection is another case in which we’re tied to the inject() function as we get to inject a dependency based on a condition, as the name suggests. You can’t call constructor() inside an if block, so instead, within the constructor you check a condition and call the inject() function, which is legal within the injection context. Let’s see this example:

import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { AnimationService } from './animation.service';

@Injectable({ providedIn: 'root' })
export class UiOrchestrationService {
  private readonly _animationService = isPlatformBrowser(inject(PLATFORM_ID))
? inject(AnimationService)
: null;

 // constructor() {
 //   const platformId = inject(PLATFORM_ID);
 //   if (isPlatformBrowser(platformId)) {
 //     this.animationService = inject(AnimationService);
 //   }
 // }

  triggerAnimation() {
    this.animationService?.start();
  }
}

In the above example we have an AnimationService that runs on a browser environment only. First it injects the PLATFORM_ID which is a special token that tells you on which platform your app is running. You define the AnimationService instance as null, then check the condition inside the constructor and then inject it. With this approach you can avoid runtime errors on the server side and improve performance by not creating unnecessary services.

Other ways of using inject()

So far you have learned the concept of DI, the rules of the injection context and the difference between the inject() function and constructor-based injection. You have learned where to use injection and where not to and how strict the rules of injection context are. 

However, despite being a more advanced topic, I’d like to introduce the other ways available in Angular which allow you to use inject() where you think is not possible. This includes some helper functions.

  • runInInjectionContext:

This helper function is a way of calling inject() within an injection context while not actually being in one (e.g methods, lifecycle hooks).

// hero.service.ts
@Injectable({
  providedIn: 'root',
})
export class HeroService {
  private _environmentInjector = inject(EnvironmentInjector);
  someMethod() {
    runInInjectionContext(this._environmentInjector, () => {
      inject(SomeService); // Do what you need with the injected service
    });
  }
}

Example from https://angular.dev/guide/di/dependency-injection-context#run-within-an-injection-context

Using this method you also have to have access to the current injector.

  • injector.runInContext()

This method is used the same way as previously where runInInjectionContext is a standalone function and runInContext is a method of EnvironmentInjector.

import { inject, Injectable, EnvironmentInjector } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DataService {
  private _injector = inject(EnvironmentInjector);

  loadDataAsynchronously() {
    setTimeout(() => {
      this._injector.runInContext(() => {
        const httpClient = inject(HttpClient);
        // ...
      });
    }, 2000);
  }
}

Summary

Conventionally, dependency injection (DI) methods are strictly tied to injection context, meaning, they can’t be used outside of injection context. In this article we looked at the differences between constructor and function-based injection. Construction-based injection is the traditional method and can’t be called anywhere that will fall outside the injection context, thus we only have the inject() function in our concerns. You have learned where to use inject() function and where not to, and the ways of using it outside of injection context but making it fall inside. You have also learned how much inject() simplifies inheritance, enables conditional injection, and makes injection possible inside standalone functions. Function-based injection is the most popular way of DI to this date and that cannot be argued given these advantages and flexibility it brings to both Angular apps and developer experience.

Related article: https://angular.love/dependency-injection-in-angular-everything-you-need-to-know

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.