When applications make HTTP requests and fail, we need to handle them. Ideally, we take care of these errors in one place in our code. In this article, we discover how to handle these exceptions in Angular by using an interceptor. We look into the best steps to take directly in the interceptor before sending errors on their way to the error handler.
What are Angular interceptors?
Interceptors are unique Angular services that we can implement to add behavior to HTTP requests in our application. HttpInterceptor provides a way to intercept HTTP requests and responses. In this sense, each interceptor can handle the request entirely by itself.
As the diagram above shows, the interceptors are always in the middle of an HTTP request. As middlemen, they allow us to perform operations on the requests on their way to and back from the server, making it a perfect place to centralize code for things like adding headers, passing tokens, caching, and error handling.
What is an error interceptor?
An error interceptor is a special kind of interceptor used for handling errors that happen when making HTTP requests. Errors come either from the client-side (browser) or the server-side when the request fails for some reason. If the request fails on the server, HttpClient returns an error object instead of a successful response. When an error occurs, you can inform the user by reacting to the error details, or in some cases, you might want to retry the request.
If you are looking into more ways of using interceptors, then this article has plenty of them:
Implementing the interceptor
To create an interceptor, declare a class that implements the intercept()
method of the HttpInterceptor
interface:
import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request);
}
}
The intercept()
method lets us inspect or alter a request
. The next
object represents the next interceptor in the chain of interceptors.
Providing the interceptor
The ErrorInterceptor is a service that we must provide before the app can use it:
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
]
})
export class AppModule {}
Now that we set up the interceptor, we can start powering it up with some error handling capabilities.
The retry strategy
As an alternative to re-throwing errors, we can retry to subscribe to the errored out Observable. For example, network interruptions can happen in mobile scenarios, and trying again can produce a successful result. RxJS offers several retry operators. For example, the retry()
operator automatically re-subscribes a specified number of times, in effect reissuing the HTTP request. The following example shows how to retry a failed request:
import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import {retry} from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(retry(3)); // Retry failed request up to 3 times.
}
}
This strategy might save us from a few errors, but more often than not, it creates unneeded requests. Let’s see how we can minimize them.
Retry when?
To make our retry strategy smarter, we can use retryWhen()
, which provides a mechanism for retrying errors based on custom criteria. We have three conditions for our smart retry:
- Retry twice at most
- Only retry 500 internal server errors
- Wait before retrying
With these conditions, we figure we give whatever is causing the exception a chance to recover and only retry to errors that can succeed if we try again.
import {Injectable} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable, of, throwError} from 'rxjs';
import {mergeMap, delay, retryWhen} from 'rxjs/operators';
export const maxRetries = 2;
export const delayMs = 2000;
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request).pipe(
retryWhen((error) =>
return error.pipe(
mergeMap((error, index) => {
if (index < maxRetries && error.status == 500) {
return of(error).pipe(delay(delayMs));
}
throw error;
})
)
)
)
}
}
The index
from mergeMap()
tells us which try we are on to stop retrying when we reach our limit. We can then check the status of the exception. And depending on the error status
, we can decide what to do. In this example, we retry twice with a delay when we get the error status 500. All remaining errors are re-thrown for further handling.
If you are interested in the whole picture, check out my article:
Conclusion
In this guide, we looked at different ways to handle failed HTTP requests using the power of RxJS. Applying different retry strategies can help us specify what should happen and when if the unexpected happens. All this might seem like a small detail, but saving users from errors will make both them, the support personnel, and in the end, the developers happy.