Skip to content

feat(eslint-plugin): [no-inject-outside-di-context] add rule#2892

Closed
cyrilletuzi wants to merge 12 commits into
angular-eslint:mainfrom
cyrilletuzi:feat/eslint-plugin/no-inject-outside-di-context
Closed

feat(eslint-plugin): [no-inject-outside-di-context] add rule#2892
cyrilletuzi wants to merge 12 commits into
angular-eslint:mainfrom
cyrilletuzi:feat/eslint-plugin/no-inject-outside-di-context

Conversation

@cyrilletuzi

@cyrilletuzi cyrilletuzi commented Feb 5, 2026

Copy link
Copy Markdown

Hello,

inject() is now the official recommended way to inject dependencies in Angular, and the prefer-inject rule is in the recommended set of Angular ESLint since v20.

While this new syntax has been done for some reasons I will not expand here, there is one setback: inject() must be used in an "injection context". There is 2 main issues with this:

  • the concept of "injection context" is obscure for beginners (and not well documented enough)
  • if outside an injection context, compilation is fine but a runtime error happens, which I see as a major setback in Angular and TypeScript philosophy (it is much more difficult to debug, and all other consequences)

The 2 main errors developers do:

  • using inject() in ngOnInit() or other methods
  • using inject() in a constructor or field initializer, but inside a callback where the injection context is lost (for example inside the callback of an Observable operator or a Promise)

This rule aims to fix all these problems by checking inject() is used only in allowed places.

It was quite a challenge, but I think I manage to take into account all places where the injection context is available:

  • Component / Directive / Pipe / Injectable / NgModule field initializers
  • Component / Directive / Pipe / Injectable / NgModule constructors
  • special contexts (mainly guards, resolvers and interceptors), both in new function syntax and legacy class syntax
  • options of a route (inline guard, inline resolver, and other route properties accepting a callback)
  • factories of an InjectionToken, a provider or an Injectable
  • in explicit runInInjectionContext()
  • in some application providers (like provideAppInitializer(), providePlatformInitializer(), provideEnvironmentInitializer(), and view transitions options)
  • in custom injectable functions where injection context is asserted via assertInInjectionContext()
  • but never in a nested callback or after an await (which is equivalent to be in a .then() callback)

This list was possible by the combination of:

About rule name and future-proof consistency: I hesitated between no-inject-outside-di-context and no-injection-outside-di-context. Indeed, this PR could easily be the base of other similar rules, as other functions like effect() or toSignal() require an injection context. But note that the scenario is not exactly the same for 2 reasons:

  • these functions accept an injector as a parameter if outside an injection context (which inject() does not)
  • the purpose is not the same: these functions require an injection context mainly to be able to be destroyed properly (while inject() requires an injection context by its own nature and purpose)

So it would be more a generalization of #2803, and the future other rule could be called no-implicit-injector or no-implicit-injection-context.

Also, while I tried be as generic as possible, part of this rule code needed to be hard-coded (special functions names for example). While it will not change every day, I volunteer for maintaining this rule (my work allows me to keep a close eye on Angular releases and details).

Thanks to @JamesHenry for the amazing work, and to @rznn7 for #2803 which served as a model and base for this new rule.

Fixes #2300

@cyrilletuzi cyrilletuzi force-pushed the feat/eslint-plugin/no-inject-outside-di-context branch from 379892b to a0bf84d Compare February 13, 2026 18:37
@cyrilletuzi cyrilletuzi force-pushed the feat/eslint-plugin/no-inject-outside-di-context branch from a0bf84d to 93015ad Compare February 23, 2026 17:42
@cyrilletuzi

Copy link
Copy Markdown
Author

@JamesHenry would it be possible to approve the workflow so I can check everything is OK?

@JeanMeche

Copy link
Copy Markdown

I'm not really a fan of such heuristics based rules because of the false positive/false negatives.
Since injection context is a runtime context you can't really infer statically if you're in an injection context or not.

@cyrilletuzi

Copy link
Copy Markdown
Author

@JeanMeche

Since injection context is a runtime context you can't really infer statically if you're in an injection context or not.

However the injection context is not magical, there is a list of actual and finite places where the injection context is available. I have been careful to manage all these places, even the most advanced ones, to avoid false positives.

I also have been careful to always do the checks in the most specific way possible, to avoid catching unrelated things and false negatives.

What actual examples of false positives or false negatives do you see?

Also, while I understand the general debate, other rules already exist and do the same thing (actually this one is based on #2803).

@cyrilletuzi

Copy link
Copy Markdown
Author

Also, this rule is opt-in, so it does not enforce anything to anyone.

@JeanMeche

Copy link
Copy Markdown

What actual examples of false positives or false negatives do you see?

  • synchronous callbacks. of(1).subscribe(() => /** Still a reactive context **/)
  • anything that has a runInInjectionContext up in the call stack

The only real case where you can be sure you're not in an injection context is just after an await.

I didn't like #2803, either because of the same said heuristics.

@cyrilletuzi

cyrilletuzi commented Mar 2, 2026

Copy link
Copy Markdown
Author
  • synchronous callbacks. of(1).subscribe(() => /** Still a reactive context **/)
  • anything that has a runInInjectionContext up in the call stack

Noted. So only false positives, and which are not common scenarios.

It feels far better to me to have a lint rule, and to be a little over protected, as the risk to do an inject() in the wrong place is far more problematic and more common, than having one of these false negatives (and like any rule, you can punctually disable it; or not enable it at all).

@cyrilletuzi

Copy link
Copy Markdown
Author

For people interested, I published the rule (and other rules for similar functions) in a separate package: https://github.com/cyrilletuzi/angular-eslint-injection-context

@JamesHenry

Copy link
Copy Markdown
Member

Sorry for the delay @cyrilletuzi let's stick with your community plugin for now, and we can potentially revisit in future, thanks a lot for your contribution

@JamesHenry JamesHenry closed this Jun 7, 2026
@cyrilletuzi cyrilletuzi deleted the feat/eslint-plugin/no-inject-outside-di-context branch June 7, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants