Language-Level Distinctions Between Constructor and ngOnInit

Let’s begin with the most straightforward difference, rooted in the JavaScript and TypeScript language itself. ngOnInit is simply a class method — structurally identical to any other method you might define on a class. The Angular team merely chose that particular name; any other identifier would have worked just as well:

class MyComponent {
  ngOnInit() { }
  otherNameForNgOnInit() { }
}

Whether you implement this method on a component class is entirely your choice. During compilation, the Angular compiler inspects the component to determine if this method exists and sets a corresponding flag on the class:

export const enum NodeFlags {
  ...
  OnInit = 1 << 16,

That flag later determines whether the method gets invoked on the component instance when change detection runs:

if (def.flags & NodeFlags.OnInit && ...) {
  componentClassInstance.ngOnInit();
}

A constructor, by contrast, is an entirely different construct. Whether or not you explicitly define it in your TypeScript class, it will always execute when an instance is created. The reason lies in how TypeScript transpiles class constructors into JavaScript constructor functions:

class MyComponent {
  constructor() {
    console.log('Hello');
  }
}

which compiles down to

function MyComponent() {
  console.log('Hello');
}

To instantiate the class, this function is invoked with the new operator:

const componentInstance = new MyComponent()

Even when you omit the constructor from a class, the transpilation process produces an empty function:

class MyComponent { }

compiles to a no-op function

function MyComponent() {}

This is precisely why a constructor always runs, regardless of whether you wrote it explicitly.

Differences During Component Initialization

From the perspective of component initialization, the two diverge significantly. The Angular bootstrap sequence unfolds in two major phases:

  • building the component tree
  • executing change detection

During the first phase, Angular constructs the component tree and calls each component's constructor. Lifecycle hooks such as ngOnInit fire later, during change detection. Typical initialization work relies on DI providers, input bindings, or the rendered DOM — and these become available at distinct points in the bootstrap flow.

When Angular builds the component tree, the root module injector is already configured, so any global dependency can be injected. Additionally, when a child component class is instantiated, the parent's injector is already in place, meaning providers declared on the parent — including the parent component itself — can be injected into the child. A component's constructor is the only method invoked within the injector's context, so it is the sole place to acquire dependencies. Input bindings, however, are processed later during change detection, so they are unavailable inside the constructor.

Once change detection begins, the component tree is fully constructed and every component's constructor has already been executed. At this juncture, all template nodes for each component have been inserted into the DOM. This means every piece of data needed for initialization — DI providers, DOM elements, and input bindings — is finally accessible.

For deeper insight into change detection, refer to Everything you need to know about change detection in Angular and for input processing details, see The mechanics of property bindings update in Angular.

Consider a brief example to illustrate these phases. Imagine a template structured as follows:

<my-app>
   <child-comp [i]='prop'>

Angular begins bootstrapping the application. As outlined, it first instantiates each component class. Thus, it calls the MyAppComponent constructor, resolving all injected dependencies and passing them as parameters. It also creates a DOM node serving as the host element for my-app. Next, it generates the host element for child-comp and invokes the ChildComponent constructor. At this stage, Angular does not concern itself with the i input binding or lifecycle hooks. Once this process completes, Angular has constructed the following tree of component views:

MyAppView
  - MyApp component instance
  - my-app host element data
       ChildComponentView
         - ChildComponent component instance
         - child-comp host element data

Only afterward does Angular run change detection, updating bindings for my-app and invoking ngOnInit on the MyAppComponent instance. It then updates bindings for child-comp and calls ngOnInit on the ChildComponent class.

For more about the concept of a view referenced above, consult Here is why you will not find components inside Angular.

Differences in Practical Use

Now let us examine how these two differ in everyday usage.

Constructor

In Angular, a class constructor is primarily utilized for dependency injection. This is referred to as the constructor injection pattern, detailed further here. For broader architectural considerations, Constructor Injection vs. Setter Injection by Miško Hevery offers valuable perspectives.

Nevertheless, constructors serve purposes beyond DI. The router-outlet directive from @angular/router leverages the constructor to register itself and its location (via viewContainerRef) within the router ecosystem. This technique is explored in Here is how to get ViewContainerRef before @ViewChild query is evaluated.

Despite these examples, the prevailing convention is to keep constructor logic minimal.

NgOnInit

As established, when Angular triggers ngOnInit, it has already completed component DOM creation, injected all dependencies via the constructor, and processed input bindings. Consequently, all necessary information is available, making this an ideal spot for initialization logic.

Practitioners often place initialization tasks in ngOnInit even when those tasks do not rely on DI, DOM, or input bindings.