Why handle errors with catchError and not in the subscribe error callback in Angular
Asked Answered
G

3

51

So I'd normally write my http requests like this

Service

getData() {
  return this.http.get('url')
}

Component

getTheData() {
  this.service.getData().subscribe(
    (res) => {
      //Do something
    }, 
    (err) => {
      console.log('getData has thrown and error of', err)
    })

But looking through the Angular documentation, they seem to format it like this in a Service

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
      catchError(this.handleError('getHeroes', []))
    );
}

What's the implicit upside of this as it seems quite verbose to me and I've personally never had the need to pipe my errors.

Genovevagenre answered 19/2, 2019 at 16:47 Comment(0)
D
74

1 It's all about separation of concern in Angular

One major benefit of using catchError is to separate the whole data retrieval logic including all errors that can occur along the way from the presentation of the data.

1.1 Let Components only care about the presentation of data

Components should only care about data (whether it's there or not). They shouldn't care about the specifics of how to retrieve data or all the things that could go wrong during data retrieval.

Components shouldn't fetch or save data directly and they certainly shouldn't knowingly present fake data. They should focus on presenting data and delegate data access to a service.
[Angular Tutorial - Why Services]

Let's say your data is a list of items. Your Component would call a service.getItemList() function and, as it only cares about data, would expect:

  • a list containing items
  • an empty list
  • no list i.e. null or undefined

You could easily handle all these cases with ngIf in your Component template and display the data or something else depending on the case. Having a Service function return a clean Observable that only returns data (or null) and isn't expected to throw any errors keeps the code in your Components lean as you can easily use the AsyncPipe in a template to subscribe.

1.2 Don't let Components care about data retrieval specifics like errors

Your data retrieval and error handling logic may change over time. Maybe you're upgrading to a new Api and suddenly have to handle different errors. Don't let your Components worry about that. Move this logic to a Service.

Removing data access from components means you can change your mind about the implementation anytime, without touching any components. They don't know how the service works. [Angular Tutorial - Get hero data]

1.3 Put the data retrieval and error handling logic in a Service

Handling errors is part of your data retrieval logic and not part of your data presentation logic.

In your data retrieval Service you can handle the error in detail with the catchError operator. Maybe there are some things you want to do on all errors like:

  • log it
  • display a user oriented error message as a notification (see Show messages)
  • fetch alternative data or return a default value

Moving some of this into a this.handleError('getHeroes', []) function keeps you from having duplicate code.

After reporting the error to console, the handler constructs a user friendly message and returns a safe value to the app so it can keep working. [Angular Tutorial - HTTP Error handling]

1.4 Make future development easier

There may come a time when you need to call an existing Service function from a new Component. Having your error handling logic in a Service function makes this easy as you won't have to worry about error handling when calling the function from your new Component.

So it comes down to separating your data retrieval logic (in Services) from your data presentation logic (in Components) and the ease of extending your app in the future.

2 Keeping Observables alive

Another use case of catchError is to keep Observables alive when you're constructing more complex chained or combined Observables. Using catchError on inner Observables allows you to recover from errors and keep the outer Observable running. This isn't possible when you're using the subscribe error handler.

2.1 Chaining multiple Observables

Take a look at this longLivedObservable$:

// will never terminate / error
const longLivedObservable$ = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes())
);
longLivedObservable$.subscribe(console.log);

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl).pipe(
    catchError(error => of([]))
  );
}

The longLivedObservable$ will execute a http request whenever a button is clicked. It will never terminate not even when the inner http request throws an error as in this case catchError returns an Observable that doesn't error but emits an empty array instead.

If you would add an error callback to longLivedObservable$.subscribe() and removed catchError in getHeroes the longLivedObservable$ would instead terminate after the first http request that throws an error and never react to button clicks again afterwards.


Excursus: It matters to which Observable you add catchError

Note that longLivedObservable$ will terminate if you move catchError from the inner Observable in getHeroes to the outer Observable.

// will terminate when getHeroes errors
const longLivedObservable = fromEvent(button, 'click').pipe(
  switchMap(event => this.getHeroes()),
  catchError(error => of([]))
);
longLivedObservable.subscribe(console.log); 

getHeroes(): Observable<Hero[]> {
  return this.http.get<Hero[]>(this.heroesUrl);
}

"Error" and "Complete" notifications may happen only once during the Observable Execution, and there can only be either one of them.

In an Observable Execution, zero to infinite Next notifications may be delivered. If either an Error or Complete notification is delivered, then nothing else can be delivered afterwards.
[RxJS Documentation - Observable]

Observables terminate when an Error (or Complete) notification is delivered. They can't emit anything else afterwards. Using catchError on an Observable doesn't change this. catchError doesn't allow your source Observable to keep emitting after an error occurred, it just allows you to switch to a different Observable when an error occurs. This switch only happens once as only one error notification can be delivered.

In the example above, when this.getHeroes() errors this error notification is propagated to the outer stream leading to an unsubscribe from fromEvent(button, 'click') and catchError switching to of([]).

Placing catchError on the inner Observable doesn't expose the error notification to the outer stream. So if you want to keep the outer Observable alive you have to handle errors with catchError on the inner Observable, i.e. directly where they occur.


2.2 Combining multiple Observables

When you're combining Observables e.g. using forkJoin or combineLatest you might want the outer Observable to continue if any inner Observable errors.

const animals$ = forkJoin(
  this.getMonkeys(), 
  this.getGiraffes(), 
  this.getElefants()
);
animals$.subscribe(console.log);

getMonkeys(): Observable<Monkey[]> {
  return this.http.get<Monkey[]>(this.monkeyUrl).pipe(catchError(error => of(null)));
}

getGiraffes(): Observable<Giraffe[]> {
  return this.http.get<Giraffe[]>(this.giraffeUrl).pipe(catchError(error => of(null)));
}

getElefants(): Observable<Elefant[]> {
  return this.http.get<Elefant[]>(this.elefantUrl).pipe(catchError(error => of(null)));
}

animals$ will emit an array containing the animal arrays it could fetch or null where fetching animals failed. e.g.

[ [ Gorilla, Chimpanzee, Bonobo ], null, [ Asian Elefant, African Elefant ] ]

Here catchError allows the animals$ Observable to complete and emit something.

If you would remove catchError from all fetch functions and instead added an error callback to animals$.subscribe() then animals$ would error if any of the inner Observables errors and thus not emit anything even if some inner Observables completed successfully.

To learn more read: RxJs Error Handling: Complete Practical Guide

Defeatism answered 20/2, 2019 at 16:6 Comment(5)
Thank you that actually makes perfect sense and I appreciate the effort you've put into your answer to explain the entire process.Genovevagenre
Great answer, can you provide more details about why does "It matters to which Observable you add catchError" ?Hengelo
Sometimes, it is beneficial to handle the http status codes in the service, and then return an application specific object encapsulating the error. This extra layer of mapping allows you to decouple the HTTP layer dependency while still maintaining the ability for a consumer of the service (component) to provide local error responses instead of being forced to feed errors into some global notification system. I think this fits under your 1.3 and 1.4. For example, you might return an AddUserResponse | AddUserError from a service method. The AddUserError would be built from the http error.Acrospire
I still dont see how the erroor is handled by the subscribePonce
@Ponce it really isn't handled as an error by subscribe. Your are returning an empty array into the success block. If you need to alert your user that an error exists or if you need to different types of messages for different error codes then your subscribe block can get a little more involved with logic and it will be messy. Error handling is tricky and there is no silver bullet for all scenarios.Cockchafer
O
3

According to Angular team
"handleError() method reports the error and then returns an innocuous result so that the application keeps working"

Because each service method returns a different kind of Observable result, function in catchError like handleError() here takes a type parameter so it can return the safe value as the type that the app expects.

Octodecillion answered 19/2, 2019 at 17:44 Comment(2)
Not sure I follow to be quite honest, why would having an error being thrown stop the application from working? Surely it would just throw the error on the call and you keep going about your business? And what do you mean by a safe value as opposed to to a non safe one?Genovevagenre
@Genovevagenre For detail info you can check this example at angular.io/tutorial/toh-pt6 . all the bestOctodecillion
G
1

Just came across this and thought I'd update my findings to better answer my own question.

While the main point of abstracting away the error handling logic from the component is a totally valid point and one the primary ones, there are several other reasons why the catchError is used over just handling the error with the subscription error method.

The primary reason is that the catchError allows you to handle the returned observable from either the http.get or the first operator that errors within a pipe method i.e.:

this.http.get('url').pipe(
  filter(condition => condition = true), 
  map(filteredCondition => filteredCondition = true), 
  catchError(err => {
    return throwError(err);
  })
).subscribe(() => {});

So if either of those operators fails, for whatever reason, the catchError will catch the observable error returned from that, but the major benefit that I've come across using catchError is that it can prevent the observable stream from closing in the event of an error.

Using the throwError or catchError(err throw 'error occcured') will cause the error portion of the subscription method to be invoked thus closing the observable stream, however using catchError like so:

Example one:

// Will return the observable declared by of thus emit the need to trigger the error on the subscription
catchError(err, of({key:'streamWontError'}));

Example two:

// This will actually try to call the failed observable again in the event of the error thus again preventing the error method being invoked. 
catchError(err, catchedObservable});
Genovevagenre answered 23/7, 2019 at 15:16 Comment(1)
syntax error, catchError only accepts 1 argumentPonce

© 2022 - 2024 — McMap. All rights reserved.