Ben Lesh has written an excellent piece titled “Don’t Unsubscribe”, making the case for avoiding explicit unsubscriptions in favor of mechanisms that handle them implicitly. Following that reasoning, I was tempted to call this article “Don’t Even Subscribe”, but the reality is that a handful of situations do demand explicit subscriptions — hence the more measured title above.

So, when is subscribing the right call? The short answer: only when there’s no other option. One major benefit of never subscribing is that you also never need to worry about unsubscribing.

But when is it truly unavoidable?

Services: Almost Never

As far as I can determine, there is no scenario where a service must subscribe to an Observable.

Services typically act as data providers. A component or directive requests data, the service returns an Observable that will eventually emit that data, and the UI layer handles the subscription. The service itself has no need to subscribe.

Now, a service may call another service and receive an Observable in return. As the author of the outer service, you might be tempted to subscribe to it. But remember: that service call was only made because something else invoked your service. Ultimately, there is always a UI element — a component or directive — at the top of the chain, and if you simply pass the Observable through, that UI element can take care of the subscription.

One edge case: a service method receives an Observable and its job is to perform some action with the data that Observable eventually emits. Even here, the better approach is not to subscribe directly, but rather to create a new Observable that signals completion, leaving the subscription to the caller.

For instance, suppose a service is handed an Observable and asked to collect all its emissions and forward them to another service. One could subscribe and, upon completion, write the data. But a cleaner design is:

class AccumLogService {
  constructor(private simpleLogService: SimpleLogService) {}
  logAllThisData(data: Observable<string>): Observable<void> {
    return data.pipe(reduce((acc: string[], v: string[]) => acc.concat([v]), []),
                     concatMap(total => this.simpleLogService.logArray(total)));
  }
}

This way, the responsibility for subscription falls to the calling function, which is where it belongs.

There’s a natural tendency among developers to build “fire-and-forget” services that return hot Observables already running. Experience, however, shows this is rarely a good idea. In some cases, fire-and-forget simply doesn’t make sense, and mixing hot and cold Observables across methods leads to confusion. It’s better to adopt a consistent rule: the caller always subscribes.

Is there any situation where returning an Observable isn’t possible, or where a service should retain a subscription — say, for cancellation purposes? I can’t say with absolute certainty, but I have never encountered one, nor have I been able to invent a plausible example.

The answer to “When should I subscribe in a service?” is: only when absolutely necessary — and that almost never happens.

Components: A Few Exceptions

In practice, many components misuse subscriptions. A common (and problematic) pattern looks like this:

/**
 * terrible use of .subscribe()
 * Do NOT do this
 */
@Component({
  template: `The current value is {{currentValue}}`,
})
export class SomeComponent {
  currentValue: string;
  constructor(someService: SomeService) {
    someService.getSomeObservable().subscribe(v => {
      this.currentValue = v;
    });
  }
}

The first thing to notice is the memory leak. The subscription is never cleaned up, so if the Observable never completes, the component, its template, and everything attached to them will stay in memory indefinitely.

You could patch it with something like:

/**
 * very bad use of .subscribe()
 * Do NOT do this either
 */
@Component({
  template: `The current value is {{currentValue}}`,
})
export class SomeComponent implements OnDestroy {
  private readonly onDestroy = new Subject<void>();
  currentValue: string;
  constructor(someService: SomeService) {
    someService.getSomeObservable()
      .pipe(takeUntil(this.onDestroy))
      .subscribe(v => {
      this.currentValue = v;
    });
  }
  ngOnDestroy() {
    this.onDestroy.next();
  }
}

(For more on the takeUntil() pattern, see this discussion.)

That revision is better — it doesn’t leak — but it’s still needlessly intricate. It copies data into the component, which has no real use for it beyond passing it to the template. Worse, after the state changes, the template must be refreshed. With the default CheckAlways strategy, that happens automatically, but CheckAlways is inefficient. With OnPush — generally the preferable choice — you must remember to call markForCheck() on the ChangeDetectorRef.

The “subscribe and stash data in component state” pattern is unhealthy. It drags you into change-detection intricacies and, for our purposes, it introduces an unnecessary subscription.

A better approach — more performant, clearer, and more idiomatic — is to let Angular handle the Observable directly with the async pipe:

@Component({
  template: `The current value is {{currentValue | async }}`,
})
export class SomeComponent  {
  constructor(private someService: SomeService) {}
  
  currentValue = this.someService.getSomeObservable()
                     .pipe(share());
}

(See this article for more on swapping subscriptions for async.)

So, can we answer “when to subscribe in components?” with the same mantra — only when necessary, which is nearly never?

Unfortunately, no. There are at least two situations where explicit subscriptions in components or directives are justified.

The first is when the Observable triggers a side effect in the outside world — typically an HTTP POST, DELETE, or PUT aimed at updating a backend.

The second is when the component itself — not Angular via a template — consumes the data. This happens, for instance, when opening a modal dialog or displaying a user-facing notification like a snackbar (see examples).

In both cases, the component is the entity that actually wants the Observable to run, so it should be the one to subscribe.

Often, these two cases coincide: a user requests a permanent change, the code performs it, and then informs the user. A simple example:

@Component({
  template: `<button (click)="buttonPress.next()">Press Me</button>`,
})
export class SomeComponent implements OnDestroy {
  private readonly onDestroy = new Subject<void>();
  readonly buttonPress = new Subject<void>();
  
  constructor(someService: SomeService,
              snackBar: MatSnackBar) {
    this.buttonPress.pipe(
      concatMap(() => someService.logButtonPress()),
      takeUntil(this.onDestroy))
    .subscribe(v => {
        this.snackBar.open("Button Press Logged!");
    });
  }
  ngOnDestroy() {
    this.onDestroy.next();
  }
}

There’s some debate about this approach. The code modifies the outside world in two places — logButtonPress() writes to the log, and the snackbar tells the user — which is why a subscription is needed. However, the first side effect occurs inside concatMap(), not in the subscription itself, which slightly violates the principle that operators should contain only pure functions.

The alternative is nested subscriptions: subscribe to the button press, and within that callback invoke logButtonPress(), subscribe to its result, and show the snackbar in that inner subscription.

Most experts agree the first approach is the lesser evil, partly because nesting could easily spiral beyond two levels — you might have multiple asynchronous mutations and multiple UI updates to handle upon their completion.

No matter how you structure it, though, at least one subscription will be required.

So, despite the catchy title, subscribing is necessary more often than unsubscribing — but still, only when it truly is.

Thanks to Alex Okrushko and Max Koretskyi.