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.
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.
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:
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.
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]
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:
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]
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.
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.
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.
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
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.
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});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With