๐ Angular ESLint: no-implicit-take-until-destroyed rule
Did you know you can use ESLint to ensure that the takeUntilDestroyed operator is called within an Angular injection context?
๐ Angular ESLint: no-implicit-take-until-destroyed rule

Did you know you can use ESLint to ensure that the takeUntilDestroyed operator is called within an Angular injection context?
When Angular made the inject function public, they introduced the concept of an injection context. In short, there is a fixed set of conditions under which you can call inject so it can implicitly obtain dependencies from an injector. Giving implicit access to the injector (and thus dependency injection) from within a function unlocked a lot of possibilities thanks to easy function composability.
By using inject together with DestroyRef, Angular introduced the takeUntilDestroyed operator, which allows you to implicitly unsubscribe from an RxJS stream when, for example, a component is destroyed. The key thing is that you must use takeUntilDestroyed within an injection context:
import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
protected readonly userInfo$ = this.userService.getUserInfo().pipe(takeUntilDestroyed());
ngOnInit() {
this.userInfo$.subscribe((userInfo) => {
// do sth with userInfo
});
}
}
If you fail to do so, you will get a runtime error:
import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
ngOnInit() {
this.userService
.getUserInfo()
// ๐จ ๐จ ๐จ runtime error
.pipe(takeUntilDestroyed())
.subscribe((userInfo) => {
// do sth with userInfo
});
}
}
The downside is that you only discover this bug at runtime, when you launch your application. Fortunately, you can enable the no-implicit-take-until-destroyed ESLint rule to catch these issues immediately in your IDE or CI pipeline:
import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
ngOnInit() {
this.userService
.getUserInfo()
// ๐ฎ ๐จ
// @angular-eslint/no-implicit-take-until-destroyed
// `takeUntilDestroyed()` must be called with an explicit `DestroyRef` parameter
// when used outside of an injection context.
.pipe(takeUntilDestroyed())
.subscribe((userInfo) => {
// do sth with userInfo
});
}
}
You can simply fix it by providing DestroyRef explicitly:
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
this.userService
.getUserInfo()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((userInfo) => {
// do sth with userInfo
});
}
}
Enabling the rule is a big improvement, but it does not catch every scenario. For example, it is perfectly valid to use the operator inside a runInInjectionContext callback, but the rule still reports a problem here โ a false positive:
import { Component, inject, Injector, OnInit, runInInjectionContext } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
private readonly injector = inject(Injector);
ngOnInit() {
runInInjectionContext(this.injector, () => {
this.userService
.getUserInfo()
// ๐จ ๐จ ๐จ NO runtime error, false positive
// @angular-eslint/no-implicit-take-until-destroyed
// `takeUntilDestroyed()` must be called with an explicit `DestroyRef` parameter
// when used outside of an injection context.
.pipe(takeUntilDestroyed())
.subscribe((userInfo) => {
// do sth with userInfo
});
});
}
}
On the other hand, some cases are not reported at all, for example when the operator usage is hidden inside a helper function:
import { assertInInjectionContext, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
export function useUserInfo(): Observable<UserInfo> {
assertInInjectionContext(useUserInfo);
return inject(UserService).getUserInfo().pipe(takeUntilDestroyed());
}
@Component({
selector: 'app-user',
template: `...`,
})
export class User implements OnInit {
private readonly userService = inject(UserService);
ngOnInit() {
// ๐จ ๐จ ๐จ runtime error, NOT reported by ESLint
useUserInfo().subscribe((userInfo) => {
// do sth with userInfo
});
}
}
Itโs definitely a good idea to use ESLint to guard correct takeUntilDestroyed usage, but be aware of its false positives and blind spots, especially with runInInjectionContext.
I hope you liked the โgolden nuggetโ ๐ , thanks for reading! ๐
๋ฉํ๋ฐ์ดํฐ
- post_id
- 9f6cc82b4472
- slug
- angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
- url
- https://medium.com/javascript-everyday/angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
- canonical_url
- https://medium.com/javascript-everyday/angular-eslint-no-implicit-take-until-destroyed-rule-9f6cc82b4472
- author_url
- https://medium.com/@wojtrawi
- status
- ok
- fetched_at
- 2026-06-14 11:28:49