Exception Handling with Angular (6.0.7)
Our target : globally catch errors.
The angular way to catch error globally is to implement a custom ErrorHandler.
Basically, I would like to act differently when we have server or client errors.
When the server notifies an error, Angular throws an HttpErrorResponse. For client-side error we will have an Error instance instead.
When the user is not authorized (401), he may be redirected to the login page.
When the user is not allowed to see a resource (403), he may be redirected to the forbidden page.
Basically, this service takes all errors information and send them to an endpoint (e.g. Help Desk).
The router navigation should only be triggered inside Angular zone.
Please take a look at my demo.
Don't hesitate to share and comment...
The angular way to catch error globally is to implement a custom ErrorHandler.
Basically, I would like to act differently when we have server or client errors.
When the server notifies an error, Angular throws an HttpErrorResponse. For client-side error we will have an Error instance instead.
When the user is not authorized (401), he may be redirected to the login page.
When the user is not allowed to see a resource (403), he may be redirected to the forbidden page.
1. The errors service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Injectable, Injector} from '@angular/core'; | |
import {LocationStrategy, PathLocationStrategy} from '@angular/common'; | |
import {HttpClient, HttpHeaders, HttpErrorResponse} from '@angular/common/http'; | |
import {Router, Event, NavigationError} from '@angular/router'; | |
import {Observable, of} from 'rxjs'; | |
// library to deal with errors: https://www.stacktracejs.com | |
import * as StackTraceParser from 'error-stack-parser'; | |
import {ErrorDetail} from '../models/error'; | |
import {ApiResponse} from '../models/api-response'; | |
/** | |
* | |
* The Errors service. | |
* | |
* @class ErrorsService | |
* | |
*/ | |
@Injectable() | |
export class ErrorsService { | |
/** | |
* @constructor | |
* | |
* @param injector - The injector service | |
* @param router - The router service | |
* | |
*/ | |
constructor( | |
private injector: Injector, | |
private router: Router | |
) { | |
// Subscribe to the NavigationError | |
this.router | |
.events | |
.subscribe((event: Event) => { | |
if (event instanceof NavigationError) { | |
// Redirect to the ErrorComponent | |
this.log(event.error) | |
.subscribe(value => { | |
this.router.navigate(['/error'], {queryParams: {msg: 'A navigation error happened.'}}); | |
}); | |
} | |
}); | |
} | |
/** | |
* Log error to console and send error to server | |
* | |
* @param error - The client | server errors | |
*/ | |
log(error: Error | HttpErrorResponse): Observable<ApiResponse> { | |
// Log the error to the console | |
console.error(error); | |
let errorDetail = this.addContextInfo(error); | |
//post to server | |
return this.sendError(errorDetail); | |
} | |
/** | |
* Create an error context information | |
*/ | |
private addContextInfo(error: Error | HttpErrorResponse): ErrorDetail { | |
let errorDetail = new ErrorDetail(); | |
errorDetail.appId = 'vmcp'; | |
errorDetail.time = new Date().getTime(); | |
errorDetail.user = 'User'; | |
if (error) { | |
errorDetail.name = error.name || null; | |
errorDetail.message = error.message; | |
errorDetail.url = this.getPath(); | |
if (error instanceof HttpErrorResponse) { | |
errorDetail.status = error.status; | |
errorDetail.stack = null; | |
} else { | |
errorDetail.status = 1000; | |
errorDetail.stack = StackTraceParser.parse(error); | |
} | |
} | |
return errorDetail; | |
} | |
/** | |
* Get path | |
*/ | |
private getPath(): string { | |
const location = this.injector.get(LocationStrategy); | |
return location instanceof PathLocationStrategy ? location.path() : ''; | |
} | |
/** | |
* Send error | |
* | |
* @param errorDetail - The error detail to be sent | |
*/ | |
private sendError(errorDetail: ErrorDetail): Observable<ApiResponse> { | |
const http = this.injector.get(HttpClient); | |
const httpOptions = { | |
headers: new HttpHeaders({ | |
'Content-Type': 'application/json', | |
'Accept': 'application/json' | |
}) | |
}; | |
console.log('Error sent to the server: ', errorDetail); | |
return http.post<ApiResponse>('api/rest/admin/errors', errorDetail, httpOptions); | |
} | |
} |
Basically, this service takes all errors information and send them to an endpoint (e.g. Help Desk).
2. The errors handler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {ErrorHandler, Injectable, Injector, NgZone} from '@angular/core'; | |
import {HttpErrorResponse} from '@angular/common/http'; | |
import {Router} from '@angular/router'; | |
import {ErrorsService} from '../services/errors.service'; | |
/** | |
* | |
* Global errors handler. | |
* | |
* @class ErrorsHandler | |
* @implements ErrorHandler | |
* | |
*/ | |
@Injectable() | |
export class ErrorsHandler implements ErrorHandler { | |
private serverErrorMsg: string = 'We\'re sorry! The server has encountered an internal error and was unable to complete your request.'; | |
private clientErrorMsg: string = 'We encountered an error and could use your help. Please contact us and tell us what you were doing when this error occured.'; | |
/** | |
* @constructor | |
* | |
* @param injector - We use the injector service because | |
* this error handler is instantiated before the providers. | |
* @param zone - The NgZone | |
*/ | |
constructor( | |
private injector: Injector, | |
private zone: NgZone | |
) {} | |
/** | |
* Handle error method | |
* | |
* @param error - The errors | |
*/ | |
handleError(error: Error | HttpErrorResponse) { | |
//http errors | |
if (error instanceof HttpErrorResponse) { | |
if (!navigator.onLine) { | |
// No Internet connection | |
this.redirectToErrorPage('No Internet Connection.'); | |
} else { | |
if (error.status === 400) { | |
//business errors | |
this.redirectToErrorPage(error.message); | |
} else { | |
this.notifyHelpDesk(error, this.serverErrorMsg); | |
} | |
} | |
} else { | |
// Client Error Happend | |
this.notifyHelpDesk(error, this.clientErrorMsg); | |
} | |
} | |
private notifyHelpDesk(error: Error | HttpErrorResponse, msg: string) { | |
const errorsService = this.injector.get(ErrorsService); | |
errorsService.log(error).subscribe(value => { | |
this.redirectToErrorPage(msg); | |
}); | |
} | |
private redirectToErrorPage(msg: string) { | |
const router = this.injector.get(Router); | |
//Router navigation should only be triggered inside Angular zone | |
this.zone.run(() => { | |
router.navigate(['/error'], {queryParams: {msg: msg}}); | |
}); | |
} | |
} |
The router navigation should only be triggered inside Angular zone.
3. The core routing module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NgModule, ErrorHandler } from '@angular/core'; | |
import { Routes, RouterModule } from '@angular/router'; | |
import { HTTP_INTERCEPTORS } from '@angular/common/http'; | |
import { ErrorsService } from './services/errors.service'; | |
import { ErrorsHandler } from './handlers/errors.handler'; | |
import { ContactService } from './services/contact.service'; | |
import { ErrorComponent } from './components/error.component'; | |
import { EntryComponent } from './components/entry.component'; | |
const routes: Routes = [ | |
{ | |
path: 'error', | |
component: ErrorComponent | |
} | |
] | |
@NgModule({ | |
imports: [RouterModule.forRoot(routes)], | |
exports: [RouterModule] | |
}) | |
export class CoreRoutingModule { } | |
export const CORE_COMPONENTS_EXPORTS = [ | |
EntryComponent | |
]; | |
export const CORE_COMPONENTS = [ | |
ErrorComponent, | |
EntryComponent | |
]; | |
export const CORE_PROVIDERS: any[] = [ | |
ErrorsService, | |
{ | |
provide: ErrorHandler, | |
useClass: ErrorsHandler, | |
}, | |
ContactService | |
] |
Please take a look at my demo.
Don't hesitate to share and comment...
Comments
Post a Comment