Reputation: 2211
I was trying to implement a general ErrorHandler for my Angular4 application with the aim to display a dialog with some information on the error I receive.
This is the error handler:
import { ErrorHandler, Injectable, Injector } from '@angular/core'
// import { Router } from '@angular/router'
import { MatDialog } from '@angular/material'
import { HttpErrorResponse } from '@angular/common/http'
import { MessageDialog } from './message.dialog'
// import { LoginService } from '../login/main.service'
@Injectable()
export class DialogErrorHandler implements ErrorHandler {
constructor(private injector: Injector) {
}
handleError(error: any): void {
let localError = error;
let finalMessage: string = "Errore sconosciuto";
let finalCallback: () => void = () => { console.log("default callback")};
let dialog: MatDialog = this.injector.get(MatDialog);
// let login: LoginService = this.injector.get(LoginService);
// let router: Router = this.injector.get(Router);
if( localError instanceof HttpErrorResponse && localError.error instanceof Error){
localError = localError.error
}
if (localError instanceof HttpErrorResponse) {
console.log("http error");
let errorDesc = localError.status + " " + localError.statusText + ": ";
switch (localError.status) {
case 403:
finalMessage = "La sessione è scaduta, ripeti il login.";
// finalCallback = () => { login.logout(); router.navigate['/login']; };
break;
case 500:
finalMessage = "Errore sul server - " + errorDesc + localError.error;
break;
default:
finalMessage = errorDesc;
}
} else {
finalMessage = localError.toString();
finalCallback = () => { location.reload() };
}
dialog.open(MessageDialog, {
data: {
message: finalMessage,
callback: finalCallback
}
})
console.log(error);
}
}
While this is the dialog:
import { Component, Inject } from '@angular/core'
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'
@Component({
selector: 'message-dialog',
template: `
<p mat-dialog-title color="primary" class="centered">Attenzione</p>
<div mat-dialog-content>{{message}}</div>
<div mat-dialog-actions style="display: flex; justify-content: center">
<button mat-button mat-dialog-close (click)="handleClick()">
OK
</button>
</div>
`,
styleUrls: ['../common/style.css']
})
export class MessageDialog {
message: string = "cose";
callback: () => void;
constructor( @Inject(MAT_DIALOG_DATA) private data,
private diagref: MatDialogRef<MessageDialog>) {
if (data) {
this.message = data.message;
this.callback = data.callback;
}
}
handleClick() {
this.diagref.close();
if (this.callback) {
this.callback();
}
}
}
This is a plunkr with what I have come to: https://plnkr.co/edit/dZ9yNf?p=preview
Now, the problem is that after I launch an error, the MatDialog I display doesn't show the message and doesn't close. It correctly executes the callback function, but it stays there.
What am I doing wrong? Why this strange behavior? Do you have any advice?
P.S. The commented stuff is stuff I will need but that I have been able to take out to simplify the handler without solving the issue. It's there, will be uncommented, but it is not the problem.
EDIT: After 9 days, a tumbleweed badge and other stuff, I have been able to reproduce the error. Same plunkr, the difference is in the source of the error. In particular:
handleClick(){
Observable.throw(new Error("local error")).subscribe(
() => {},
(err) => {throw err)
)
}
throwError(){
this.http.get("http://www.google.com/thisshouldnotexist").subscribe(
() => {},
(err) => {throw err}
)
}
In this scenario, handleClick
goes smoothly, while throwError
hangs there. If anybody knows a solution, please help.
Upvotes: 2
Views: 1698
Reputation: 2211
Seems like I have not answered myself after solving it. Turns out it was a bug of angular here the issue I opened, but not the bug you might think. Actually the wanted behaviour is the "non closing" one, which is actually the UI not updating.
The reason is that after an error the error handler is executed outside the NgZone, which means that angular won't know about UI changes you make. ( I had to google zone.js to understand what all this was about. )
The solution is to inject the NgZone again and run stuff manually inside the zone. It works, but since errors inside the zone get sent back to the handler, it might cause an infinite loop ( error, operation to show the error causes a new error, the new error trigger the same operation that throws the error again and so on...). What I did to avoid that is to put a guard to do not show more than one error to the UI, so that unless the user tells me that it is ok, any subsequent error is just sent to console. Here is the working code:
import { ErrorHandler, Injectable, Injector, NgZone } from '@angular/core'
import { Router } from '@angular/router'
import { MatDialog } from '@angular/material'
import { HttpErrorResponse } from '@angular/common/http'
import { MessageDialog } from '../dialogs/message.dialog'
import { LoginService } from '../login/main.service'
@Injectable()
export class DialogErrorHandler extends ErrorHandler {
private elaborating: boolean = false;
constructor(private injector: Injector, private ngzone: NgZone) {
super();
}
handleError(error: any): void {
if (!this.elaborating) {
this.elaborating = true;
let localError = error;
let finalMessage: string = "Errore sconosciuto";
let finalCallback: () => void = () => { console.log("default callback") };
let dialog: MatDialog = this.injector.get(MatDialog);
let login: LoginService = this.injector.get(LoginService);
let router: Router = this.injector.get(Router);
// nessun dialog per TypeError, evita problemi con MatSelect
if (localError instanceof TypeError) {
this.elaborating = false;
} else {
if (localError instanceof HttpErrorResponse && localError.error instanceof Error) {
localError = localError.error
}
if (localError instanceof HttpErrorResponse) {
let errorDesc = 'Request to ' + localError.url + "\n" + localError.status +
" " + localError.statusText + ": " + localError.error;
switch (localError.status) {
case 403:
finalMessage = "La sessione è scaduta, ripeti il login.";
finalCallback = () => { login.logout(); router.navigate['/login']; };
break;
case 500:
finalMessage = "Errore sul server - " + errorDesc;
break;
default:
finalMessage = errorDesc;
}
} else {
finalMessage = localError.message;
}
this.ngzone.run(() => {
dialog.open(MessageDialog, {
data: {
message: finalMessage,
callback: () => { finalCallback(); this.elaborating = false; }
}
})
})
}
}
super.handleError(error);
}
}
I inject the NgZone in the handler and use it to show the dialog, and I have the elaborating boolean that should avoid infinite recursion.
Upvotes: 6