Error Handling in Observables
Have you ever faced a situation,
When a observable errors out and you are unable to recover from the error?
When you want to retry an http request a certain number of times or unlimited number of times with a certain delay?
Lets find the answers to these questions and much more…
Before diving into error handing the most important thing to understand is that the lifecycle of an observable ends when either of the 2 happen: an observable errors out or an observable completes.
When I say that the lifecycle of an observable ends, I mean that it wont emit any more values.
An observable cannot both error and complete in a given execution. It can either error or complete.
Lets also remember that a subscriber handler takes 3 optional arguments:
A success handler function which is called each time the observable emits a value
A error handler function which is called only once when the observable errors out.
A completion handler function which is also called just once when the observable has emitted all the values and has not errored out.
Lets begin with an example where I can retrieve information about users with Ids ranging 1–10.
Template:
<input type=”text” [(ngModel)]=”somedata” name=”somedata” id=”userIds”>
Component:
Every 3 secs, the latest number(user id)typed by the user is passed to the switchMap.This operator maps the user id to an inner observable via the getSpecificUser() in the service. This operator also subscribes to the inner observable.
fromEvent(document.getElementById(‘userIds’),’keyup’).pipe(debounceTime(3000),switchMap((x:any)=>this.serv.getSpecificUser(x.target.value))).subscribe(data=>{console.log(data);},err=>{console.log(err);},()=>{console.log(“completed”);})
Service:
The function in the service retrieves fake user data for a user id.
getSpecificUser(id:number):Observable<User>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>{console.log(“Executing Http request”);}),catchError(err=>throwError(err)))}
catchError operator is similar to the catch block we use for handling errors in synchronous code in javascript.
This operator accepts a error handling function as argument. This function takes an observable an input and must produce an observable as output.
throwError operator just returns an observable that emits just a error notification. This error notification can be logged in the error handling function of the subscribe handler.
The user data for ID’s 1 and 5 get logged in the success handling function of the subscribe as below:
When I enter 0 in the text box, since there is no result for this user id, the server returns a 404 error and the catchError is executed. The throwError operator returns the error observable ,whose value is logged in the error handling function of subscribe as below:
After the observable errors out, If I enter any other valid user ID, no result gets generated. In order to recover from the error, lets re-write the function in the service:
getSpecificUser(id:number):Observable<User|string>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>{console.log(“Executing Http request”);}),catchError(err=>of("Some error")))}
Instead of re-throwing the error observable, I replace it with another observable which emits a string message “Some error”.
I first successfully got the result for userId 8.
Next I entered userId 0, for which, unlike the previous time, a message “some error” gets logged in the success handling block of the subscribe.Observable didn’t error out.
Later,I am also entering another userId 4, for which I got the result despite the 404 error the previous time.
retry vs retryWhen vs repeat
retry and retryWhen execute only when an observable errors out.
If you want re-execute an observable a specific number of times, you can go for retry. In the below example, in case the observable errors out, it will re-execute 2 more times and then the observable (success/error) will be returned by catchError operator and logged in the subscribe handler.
getSpecificUser(id:number):Observable<User>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>{console.log(“Executing Http request”)}),catchError(err=>throwError(err)),retry(2))}
When I enter 0, it fails the first time and it is re-tried 2 more times before logging the error notification in the subscribe handler.
retryWhen can be used when you want to re-execute the observable on error under certain conditions. Please note that retryWhen will keep re-executing the observable untill it succeeds,so it is good specify the condition when it should stop execution.
Check the example below:
maxretryAttempts=3;scalingDuration=2000;getSpecificUser(id:number):Observable<User>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>console.log(“Executing Http request”)),catchError(err=>throwError(err)),retryWhen(error=>error.pipe(mergeMap((err:Error,i:number)=>{const retryAttempt=i+1;if(retryAttempt > this.maxretryAttempts){
return throwError(err);
}
else{
console.log(“retrying in”+ “ “+retryAttempt*this.scalingDuration+” secs”);return timer(retryAttempt*this.scalingDuration);}
}))))}
When the observable errors out, I am checking if the current re-attempt is less than the max no of permitted attempts i.e 3.
If yes then I retry after a certain duration calculated as retryAttempt*this.scalingDuration secs. This means if this is my 2nd re-attempt will be after 2*2000 secs.
If no, then I throw back an error which is logged in the error handling function of subscribe block.
In the above case, I entered userId 2 in the text box, and it failed to retrieve results due to no network connectivity.
So the 1st re-attempt was done after 2000secs and 2nd re-attempt was done after 4000secs. In the 3rd re-attempt the request succeeds and I get the results.
repeat
repeat is similar to retry but for non-error cases.
Example:
getSpecificUser(id:number):Observable<User>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>{console.log(“Executing Http request”);}),delay(2000),repeat(3),catchError(err=>throwError(err)),)}
If I enter userId 2 and it successfully retrieves the results, the observable is executed 2 more times.
finalize
finalize operator is similar to the finally block for synchronous code in Javascript. It gets executed irrespective of success or failure.
This operator executes a callback function when an observable errors or completes.
Example:
getSpecificUser(id:number):Observable<User>{return this.http.get<User>(‘https://jsonplaceholder.typicode.com/users/'+id).pipe(tap(()=>{console.log(“Executing Http request”);}),catchError(err=>throwError(err)),finalize(()=>console.log(“Peforming cleanup”)))}
As you can see, irrespective of whether the observable succeeds or errors out, the finalize operator is executed.
Please let me know your comments:)