Angular Handling Global Error to Try Again

Approximately a year ago, I have implemented the first e2e tests on a project. It was a rather big application using JAVA SpringBoot on the back-end and Angular on the front-finish. We used Protractor every bit a testing tool, which uses Selenium. In the front-finish code there was a service, which had an error handler method. When that method was called, a modal dialog popped up and the user could see the details of the errors and the stack-trace.
The problem was that while it has tracked every error that happened on the back-end, the forepart-end failed silently. TypeErrors, ReferenceErrors and other uncaught exceptions were logged only to the console. When something went wrong during e2e test runs the screenshot, which was taken when the test step has failed, has shown absolutely cipher. Have fun debugging that!
Luckily Athwart has a built-in fashion of handling errors and information technology is extremely like shooting fish in a barrel to use. Nosotros but take to create our own service, which implements Angular's ErrorHandler interface:
import { ErrorHandler, Injectable } from '@athwart/cadre'; @Injectable({ providedIn: 'root' }) export class ErrorHandlerService implements ErrorHandler{ constructor() {} handleError(fault: any) { // Implement your own mode of treatment errors } }
While nosotros could hands provide our service in our AppModule, information technology might be a good idea to provide this service in a separate module. This way we could create our own library and apply information technology in our future projects as well:
// Fault HANDLER MODULE import {ErrorHandler, ModuleWithProviders, NgModule} from '@angular/core'; import {ErrorHandlerComponent} from './components/error-handler.component'; import {FullscreenOverlayContainer, OverlayContainer, OverlayModule} from '@angular/cdk/overlay'; import {ErrorHandlerService} from './mistake-handler.service'; import {A11yModule} from '@athwart/cdk/a11y'; @NgModule({ declarations: [ErrorHandlerComponent], imports: [CommonModule, OverlayModule, A11yModule], entryComponents: [ErrorHandlerComponent] }) export class ErrorHandlerModule { public static forRoot(): ModuleWithProviders { return { ngModule: ErrorHandlerModule, providers: [ {provide: ErrorHandler, useClass: ErrorHandlerService}, {provide: OverlayContainer, useClass: FullscreenOverlayContainer}, ] }; } }
We used the Angular CLI for generating the ErrorHandlerModule, so we already have a component generated, which can be our modal dialog's content. In order for usa to be able to put it within an Angular CDK overlay, it needs to be an entryComponent. That is why we have put it into the ErrorHandlerModule's entryComponents array.
We likewise added some imports. OverlayModule and A11yModule comes from the CDK module. They are needed for creating our overlay and to trap focus when our error dialog is opened. As yous can see, nosotros provide OverlayContainer using the FullscreenOverlayContainer course because if an error occurs, nosotros want to restrict our users' interactions to our fault modal. If we don't have a fullscreen backdrop, the users might be able to collaborate with the application and cause farther errors. Let's add together our newly created module to our AppModule:
// APP MODULE import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/cadre'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; import {MainComponent} from './master/main.component'; import {ErrorHandlerModule} from '@btapai/ng-error-handler'; import {HttpClientModule} from '@angular/mutual/http'; @NgModule({ declarations: [ AppComponent, MainComponent ], imports: [ BrowserModule, HttpClientModule, ErrorHandlerModule.forRoot(), AppRoutingModule, ], bootstrap: [AppComponent] }) export course AppModule { }
Now that we have our `ErrorHandlerService` in place, nosotros can offset implementing the logic. Nosotros are going to create a modal dialog, which displays the error in a clean, readable way. This dialog will have an overlay/backdrop and it will be dynamically placed into the DOM with the help of the Angular CDK. Let'south install it:
npm install @angular/cdk --save
According to the documentation, the Overlay component needs some pre-congenital css files. Now if we would utilise Angular Material in our projection it wouldn't be necessary, simply that is not e'er the case. Let's import the overlay css in our styles.css file. Note, that if you already use Angular Fabric in your app, you don't need to import this css.
@import '~@angular/cdk/overlay-prebuilt.css';
Permit's use our handleError method to create our modal dialog. It is important to know, that the ErrorHandler service is office of the application initialisation phase of Angular. In order to avoid a rather nasty cyclic dependency error, we use the injector as its only constructor parameter. We employ Angular'south dependency injection system when the bodily method is called. Let's import the overlay from the CDK and attach our ErrorHandlerComponent into the DOM:
// ... imports @Injectable({ providedIn: 'root' }) export course ErrorHandlerService implements ErrorHandler { constructor(private injector: Injector) {} handleError(error: any) { const overlay: Overlay = this.injector.become(Overlay); const overlayRef: OverlayRef = overlay.create(); const ErrorHandlerPortal: ComponentPortal<ErrorHandlerComponent> = new ComponentPortal(ErrorHandlerComponent); const compRef: ComponentRef<ErrorHandlerComponent> = overlayRef.attach(ErrorHandlerPortal); } }
Let's plough our attention towards our mistake handler modal. A pretty simple working solution would be displaying the mistake bulletin and the stacktrace. Let's likewise add a 'dismiss' button to the lesser.
// imports consign const ERROR_INJECTOR_TOKEN: InjectionToken<whatsoever> = new InjectionToken('ErrorInjectorToken'); @Component({ selector: 'btp-mistake-handler', // TODO: template will be implemented later template: `${mistake.bulletin}<br><button (click)="dismiss()">DISMISS</push>` styleUrls: ['./error-handler.component.css'], }) export class ErrorHandlerComponent { individual isVisible = new Subject(); dismiss$: Appreciable<{}> = this.isVisible.asObservable(); constructor(@Inject(ERROR_INJECTOR_TOKEN) public error) { } dismiss() { this.isVisible.adjacent(); this.isVisible.complete(); } }
As you can see, the component itself is pretty simple. We are going to utilise two rather of import directives in the template, to brand the dialog attainable. The first one is the cdkTrapFocus which will trap the focus when the dialog is rendered. This means that the user cannot focus elements behind our modal dialog. The 2nd directive is the cdkTrapFocusAutoCapture which will automatically focus the outset focusable chemical element within our focus trap. Also, information technology will automatically restore the focus to the previously focused element, when our dialog is airtight.
In order to be able to display the mistake's backdrop, we need to inject it using the constructor. For that, nosotros need our own injectionToken. We besides created a rather elementary logic for emitting a dismiss outcome using a subject and the dismiss$ holding. Let'south connect this with our handleError method in our service and practice some refactoring.
// imports export const DEFAULT_OVERLAY_CONFIG: OverlayConfig = { hasBackdrop: true, }; @Injectable({ providedIn: 'root' }) export class ErrorHandlerService implements ErrorHandler { private overlay: Overlay; constructor(individual injector: Injector) { this.overlay = this.injector.get(Overlay); } handleError(mistake: any): void { const overlayRef = this.overlay.create(DEFAULT_OVERLAY_CONFIG); this.attachPortal(overlayRef, fault).subscribe(() => { overlayRef.dispose(); }); } private attachPortal(overlayRef: OverlayRef, error: any): Appreciable<{}> { const ErrorHandlerPortal: ComponentPortal<ErrorHandlerComponent> = new ComponentPortal( ErrorHandlerComponent, null, this.createInjector(error) ); const compRef: ComponentRef<ErrorHandlerComponent> = overlayRef.attach(ErrorHandlerPortal); render compRef.instance.dismiss$; } individual createInjector(error: any): PortalInjector { const injectorTokens = new WeakMap<any, whatever>([ [ERROR_INJECTOR_TOKEN, fault] ]); return new PortalInjector(this.injector, injectorTokens); } }
Let's focus on providing the error as an injected parameter first. As you can see, the ComponentPortal class expects one must-have parameter, which is the component itself. The 2d parameter is a ViewContainerRef which would accept an upshot of the component's logical place of the component tree. The third parameter is our createInejctor method. Equally you can see it returns a new PortalInjector instance. Let's take a quick wait at its underlying implementation:
export class PortalInjector implements Injector { constructor( individual _parentInjector: Injector, private _customTokens: WeakMap<any, any>) { } get(token: whatsoever, notFoundValue?: any): any { const value = this._customTokens.go(token); if (typeof value !== 'undefined') { return value; } return this._parentInjector.get<any>(token, notFoundValue); } }
As you lot can encounter, information technology expects an Injector every bit a first parameter and a WeakMap for custom tokens. We did exactly that using our ERROR_INJECTOR_TOKEN which is associated with our error itself. The created PortalInjector is used for the proper instantiation of our ErrorHandlerComponent, it will make certain that the fault itself will be nowadays in the component.
At last, our attachPortal method returns the recently instantiated component'due south dismiss$ property. We subscribe to information technology, and when information technology changes we call the .dispose() on our overlayRef. And our mistake modal dialog is dismissed. Note, that nosotros also call complete on our subject area inside the component, therefore, we don't need to unsubscribe from it.
At present, this is fantabulous for errors that are thrown when there'due south an issue in the clinet side code. Simply we are creating web applications and we use API endpoints. So what happens when a Balance endpint gives back an error?
We can handle every error in its own service, but do we really want to? If everything is alright errors won't be thrown. If there are specific requirements, for example to handle 418 status code with a flying unicorn you could implement its handler in its service. But when we face rather common errors, like 404 or 503 we might want to display that in this same error dialog.
Let's just quickly gather what happens when an HttpErrorResponse is thrown. Information technology is going to happen async, and so probably nosotros are going to face up some alter detection issues. This error type has different properties than a simple error, therefore, nosotros might need a sanitiser method. Now let's get into it by creating a rather simple interface for the SanitisedError:
export interface SanitizedError { message: string; details: string[]; }
Allow's create a template for our ErrorHandlerComponent:
// Imports @Component({ selector: 'btp-error-handler', template: ` <department cdkTrapFocus [cdkTrapFocusAutoCapture]="true" class="btp-mistake-handler__container"> <h2>Mistake</h2> <p>{{error.message}}</p> <div class="btp-error-handler__scrollable"> <ng-container *ngFor="let item of error.details"> <div>{{item}}</div> </ng-container> </div> <button form="btp-error-handler__dismiss push cherry" (click)="dismiss()">DISMISS</push> </section>`, styleUrls: ['./fault-handler.component.css'], }) export class ErrorHandlerComponent implements OnInit { // ... }
We wrapped the whole modal into a <section> and we added the cdkTrapFocus directive to information technology. This directive will prevent the user from navigating in the DOM backside our overlay/modal. The [cdkTrapFocusAutoCapture]="true" makes certain that the dismiss push is focused immediately. When the modal is closed the previously focused element will go back the focus. We simply display the error message and the details using *ngFor. Allow'southward jump back into our ErrorHandlerService:
// Imports @Injectable({ providedIn: 'root' }) export class ErrorHandlerService implements ErrorHandler { // Constructor handleError(error: any): void { const sanitised = this.sanitiseError(error); const ngZone = this.injector.get(NgZone); const overlayRef = this.overlay.create(DEFAULT_OVERLAY_CONFIG); ngZone.run(() => { this.attachPortal(overlayRef, sanitised).subscribe(() => { overlayRef.dispose(); }); }); } // ... individual sanitiseError(error: Fault | HttpErrorResponse): SanitizedError { const sanitisedError: SanitizedError = { message: mistake.message, details: [] }; if (error instanceof Error) { sanitisedError.details.push(error.stack); } else if (mistake instanceof HttpErrorResponse) { sanitisedError.details = Object.keys(fault) .map((cardinal: string) => `${fundamental}: ${mistake[central]}`); } else { sanitisedError.details.button(JSON.stringify(error)); } render sanitisedError; } // ... }
With a rather uncomplicated sanitiseError method we create an object which is based on our previously defined interface. We check for mistake types and populate the data accordingly. The more interesting part is using the injector to get ngZone. When an error happens asynchronously, it usually happens outside change detection. We wrap our attachPortal with ngZone.run(/* ... */), so when an HttpErrorResponse is caught, it is rendered properly in our modal.
While the electric current state works nicely, it still lacks customisation. We utilise the Overlay from the CDK module, and then exposing an injection token for custom configurations would be nice. Some other of import shortcoming of this module is that when this module is used, some other module tin can't be used for error treatment. For instance, integrating Sentry would require you lot to implement a similar, but lightweight ErrorHandler module. In order to be able to employ both, we should implement the possibility of using hooks inside our error handler. Get-go, let's create our InjectionToken and our default configuration:
import {InjectionToken} from '@angular/core'; import {DEFAULT_OVERLAY_CONFIG} from './constants/error-handler.constants'; import {ErrorHandlerConfig} from './interfaces/mistake-handler.interfaces'; export const DEFAULT_ERROR_HANDLER_CONFIG: ErrorHandlerConfig = { overlayConfig: DEFAULT_OVERLAY_CONFIG, errorHandlerHooks: [] }; consign const ERROR_HANDLER_CONFIG: InjectionToken<ErrorHandlerConfig> = new InjectionToken('btp-eh-conf');
And so provide it with our module, using our existing forRoot method:
@NgModule({ declarations: [ErrorHandlerComponent], imports: [CommonModule, OverlayModule, A11yModule], entryComponents: [ErrorHandlerComponent] }) export form ErrorHandlerModule { public static forRoot(): ModuleWithProviders { return { ngModule: ErrorHandlerModule, providers: [ {provide: ErrorHandler, useClass: ErrorHandlerService}, {provide: OverlayContainer, useClass: FullscreenOverlayContainer}, {provide: ERROR_HANDLER_CONFIG, useValue: DEFAULT_ERROR_HANDLER_CONFIG} ] }; } }
And so integrate this config handling into our ErrorHandlerService likewise:
// Imports @Injectable({ providedIn: 'root' }) consign class ErrorHandlerService implements ErrorHandler { // ... handleError(fault: whatsoever): void { const sanitised = this.sanitiseError(mistake); const {overlayConfig, errorHandlerHooks} = this.injector.get(ERROR_HANDLER_CONFIG); const ngZone = this.injector.get(NgZone); this.runHooks(errorHandlerHooks, error); const overlayRef = this.createOverlayReference(overlayConfig); ngZone.run(() => { this.attachPortal(overlayRef, sanitised).subscribe(() => { overlayRef.dispose(); }); }); } // ... private runHooks(errorHandlerHooks: Array<(error: any) => void> = [], mistake): void { errorHandlerHooks.forEach((hook) => claw(error)); } individual createOverlayReference(overlayConfig: OverlayConfig): OverlayRef { const overlaySettings: OverlayConfig = {...DEFAULT_OVERLAY_CONFIG, ...overlayConfig}; return this.overlay.create(overlaySettings); } // ... }
And we are most ready. Permit'due south integrate a 3rd-political party error handler claw into our awarding:
// Imports const CustomErrorHandlerConfig: ErrorHandlerConfig = { errorHandlerHooks: [ ThirdPartyErrorLogger.logErrorMessage, LoadingIndicatorControl.stopLoadingIndicator, ] }; @NgModule({ declarations: [ AppComponent, MainComponent ], imports: [ BrowserModule, HttpClientModule, ErrorHandlerModule.forRoot(), AppRoutingModule, ], providers: [ {provide: ERROR_HANDLER_CONFIG, useValue: CustomErrorHandlerConfig} ], bootstrap: [AppComponent] }) export class AppModule { }
As you tin can encounter, treatment errors is an extremely important function of software development, but information technology tin can also exist fun.
Cheers very much for reading this web log post. If y'all prefer reading code, please check out my ng-reusables git repository. You tin can also endeavor out the implementation using this npm packet.
You can also follow me on Twitter or GitHub.
Learn to lawmaking for complimentary. freeCodeCamp's open source curriculum has helped more than 40,000 people get jobs as developers. Get started
williamsthemannind.blogspot.com
Source: https://www.freecodecamp.org/news/global-error-handling-in-angular-with-the-help-of-the-cdk/
0 Response to "Angular Handling Global Error to Try Again"
Post a Comment