Error Handling & Angular

Shit happens, deal with it

Aleix Suau
9 min readJan 21, 2018

If you are more than… 2 years old, you’d have realized that indeed shit happens, and your apps are not an exception.

When it happens you can ignore it, and let the bad grow, or do something, and improve the world.

What we can do with an error depends on where it comes from:

Outside Errors

Are the easier ones, you can always blame someone else. In this cases we can’t do much more than handle them, notify the user and pray to the administrator to repair it.

This errors usually come from the server and its properties have a status code starting with 5 (5xx…) and an explanation message, so we are able to know what caused it and react properly.

There are other possibilities that we can handle, like an internet connection fail, and others that we don’t, like a browser or an OS crash.

Inside Errors

This are the complex ones because they require to look ourselves in the mirror and accept that we failed, find the cause, repair it and take the lesson.

We can differentiate them based on who notifies the error:

The Server:
Seems that the properties in the server’s errors are no fixed, at least I haven’t found any standard.

A server error might contain:
- status (or code): Code status starting with 4 (4xx…). There are 28 status.
- name: The name of the error (ie: HttpErrorResponse).
- message: Explanation message (ie: Http failure response for…).

The Client (browser):
Based on a generic Error constructor, Javascript throws an Error each time something goes wrong. Most common errors are ReferenceError (call to a non-existent variable) and TypeError (ie: try to execute a variable, as it was a function). There are 5 more types.

A client error should contain:
- name (ie: ReferenceError).
- message (ie: X is not defined).
And in most modern browsers: fileName, lineNumber and columnNumber where the error happened, and stack (last X functions called before the error).

Errors, Exceptions & CallStack

In Javascript, each function that is executed is added in order of execution to the CallStack as a layer in a pile, and removed from it when the function returns.

When an Error happens, an Exception is thrown at the current point of execution and it will remove (unwind) every function of the CallStack until the exception is handled by a try/catch block. Then, the control will continue from the statement right after the catch block.

If no try/catch block is found, the Exception will remove all the functions of the CallStack, crashing completely our app.

Let’s see it in action.

Editor >>>
- Open your browser’s console.
- Open the app>app.component.ts
- Click on the ‘Fire Error’ button to see the app crash because a ReferenceError is not handled, so it unwinds all the CallStack.
- Reload the editor’s url.
- Click on the ‘Fire Error With Net’ to see the error handling.

As you can see in the console, the try/catch block prevents the app from crashing and lets the program continue right below the catch.

How to catch them all

By default, Angular comes with its own ErrorHandler that intercepts all the Errors that happen in our app and logs them to the console, preventing the app from crashing. I deactivated it in the previous example for didactic purposes…

We can modify this default behavior by creating a new class that implements the ErrorHandler:

// errors-handler.ts
import { ErrorHandler, Injectable} from '@angular/core';
@Injectable()
export class ErrorsHandler implements ErrorHandler {
handleError(error: Error) {
// Do whatever you like with the error (send it to the server?)
// And log it to the console
console.error('It happens: ', error);
}
}

Then we’ll need to tell Angular that he should use our ErrorsHandler class when a new client error happens (provide it):

// errors.module.ts
@NgModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
{
provide: ErrorHandler,
useClass: ErrorsHandler,
}
]

})

Now, every time a new error happens, Angular will log ‘It happens’ followed by the error itself in the console.

How to recognize them

Inside the ErrorHandler, we can check which kind of error it is:

// errors-handler.ts
import { ErrorHandler, Injectable} from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
export class ErrorsHandler implements ErrorHandler {
handleError(error: Error | HttpErrorResponse) {
if (error instanceof HttpErrorResponse) {
// Server or connection error happened
if (!navigator.onLine) {
// Handle offline error
} else {
// Handle Http Error (error.status === 403, 404...)
}
} else {
// Handle Client Error (Angular Error, ReferenceError...)
}
// Log the error anyway
console.error('It happens: ', error);
}

How to react to each one

Server and connection errors

Server and connection errors affect in the way the app communicates with the server. If the app is not online, the resource doesn’t exist (404), the user is not allowed to access the data (403)… HttpClient will give us an informative Error that we have to handle, but our app won’t crash and the risk of data corruption or lost can be managed.

As a general rule, I think this kind of errors need a notification; a clear message explaining to the user what is happening and how to proceed.

For this purpose, we’ll use a notification service.

// errors-handler.ts
@Injectable
()
export class ErrorsHandler implements ErrorHandler {
constructor(
// Because the ErrorHandler is created before the providers, we’ll have to use the Injector to get them.
private injector: Injector,
) { }
handleError(error: Error | HttpErrorResponse) {

const notificationService = this.injector.get(NotificationService);
if (error instanceof HttpErrorResponse) {
// Server or connection error happened
if (!navigator.onLine) {
// Handle offline error
return notificationService.notify('No Internet Connection');
} else {
// Handle Http Error (error.status === 403, 404...)
return notificationService.notify(`${error.status} - ${error.message}`);
}
} else {
// Handle Client Error (Angular Error, ReferenceError...)
}
// Log the error anyway
console.error('It happens: ', error);
}

* Here we could handle specific errors. For example, if it is a 403 error, we could notify to the user and then redirect to the login page.

Because the best Error is the one that never happens, we could improve our error handling using an HttpInterceptor that would intercept all the server calls and retry them X times before throw an Error:

// server-errors.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/retry';
@Injectable()
export class ServerErrorsInterceptor implements HttpInterceptor {

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// If the call fails, retry until 5 times before throwing an error
return next.handle(request).retry(5);
}
}

Then we’ll need to tell Angular that he should use our ServerErrorsInterceptor class in every http call:

// errors.module.ts
@NgModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ServerErrorsInterceptor,
multi: true,

},
]
})

Client errors

Client errors seem more dangerous to me. They could completely crash our app, originate corrupt data that could be stored in the server, keep the user working on stuff that wouldn’t be saved…

I think that this situations claim for a more strict response: If something is broken in our app, we need to stop the app, redirect the user to an error screen with all the information, fix it asap and inform to the user that it has been fixed.

For that we could use an ErrorComponent that takes the Error details from the route’s query params:

// errors-handler.ts
@Injectable()
export class ErrorsHandler implements ErrorHandler {
constructor(
private injector: Injector
) { }
handleError(error: Error | HttpErrorResponse) {

const notificationService = this.injector.get(NotificationService);
const router = this.injector.get(Router);
if (error instanceof HttpErrorResponse) {
// Server or connection error happened
if (!navigator.onLine) {
// Handle offline error
return notificationService.notify('No Internet Connection');
} else {
// Handle Http Error (error.status === 403, 404...)
return notificationService.notify(`${error.status} - ${error.message}`);
}
} else {
// Handle Client Error (Angular Error, ReferenceError...)
router.navigate(['/error'], { queryParams: {error: error} });
}
// Log the error anyway
console.error('It happens: ', error);
}

Editor >>>
- Click on “Client Error” to generate a Client error and see the error handling in action.
- Click on “Server Error” to generate a Server error and see the error handling in action.
- Check out app>core>errors to see the implementation of the ErrorsComponent, ErrorsHandler, ServerErrorsInterceptor and the errors-routing in detail.

How to keep track of the errors

Out of sight, out of mind… Nothing will improve if we don’t know what errors are happening in our app.

To track the errors we can use an ErrorsService that will send them to our server, filled with some useful information about the context where they happened:

//errors.service.ts
import { ErrorHandler, Injectable, Injector} from '@angular/core';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// Cool library to deal with errors: https://www.stacktracejs.com
import * as StackTraceParser from 'error-stack-parser';
@Injectable()
export class ErrorsService {
constructor(
private injector: Injector,
) { }
log(error) {
// Log the error to the console
console.error(error);
// Send error to server
const errorToSend = this.addContextInfo(error);
return fakeHttpService.post(errorToSend);
}
addContextInfo(error) {
// All the context details that you want (usually coming from other services; Constants, UserService...)
const name = error.name || null;
const appId = 'shthppnsApp';
const user = 'ShthppnsUser';
const time = new Date().getTime();
const id = `${appId}-${user}-${time}`;
const location = this.injector.get(LocationStrategy);
const url = location instanceof PathLocationStrategy ? location.path() : '';
const status = error.status || null;
const message = error.message || error.toString();
const stack = error instanceof HttpErrorResponse ? null : StackTraceParser.parse(error);
const errorToSend = {name, appId, user, time, id, url, status, message, stack};
return errorToSend;

}
}

Then, we’ll need to change our ErrorsHandler to make use of our brand new ErrorsService.

import { ErrorHandler, Injectable, Injector} from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import * as StackTraceParser from 'error-stack-parser';import { ErrorsService } from '../errors-service/errors.service';
import { NotificationService } from '../../services/notification/notification.service';
@Injectable()
export class ErrorsHandler implements ErrorHandler {
constructor(
private injector: Injector,
) {}
handleError(error: Error | HttpErrorResponse) {
const notificationService = this.injector.get(NotificationService);
const errorsService = this.injector.get(ErrorsService);
const router = this.injector.get(Router);
if (error instanceof HttpErrorResponse) {
// Server error happened
if (!navigator.onLine) {
// No Internet connection
return notificationService.notify('No Internet Connection');
}
// Http Error
// Send the error to the server
errorsService
.log(error)
.subscribe();

// Show notification to the user
return notificationService.notify(`${error.status} - ${error.message}`);
} else {
// Client Error Happend
// Send the error to the server and then
// redirect the user to the page with all the info
errorsService
.log(error)
.subscribe(errorWithContextInfo => {
router.navigate(['/error'], { queryParams: errorWithContextInfo });
});

}
}
}

Now our Errors have a lot of context info, they are sent to the server to keep track of them and they allow us to give a much better feedback and support to our user.

Editor >>>
- Click on the ‘Client Error’ button to see how now we are able even to give an incidence ID number to our user.

Once fixed, we could contact back to our user informing him/her about it. Cooool :)

There are cool services to keep track and manage all these errors from the server if you don’t want to do it yourself: https://sentry.io, https://rollbar.com or http://jsnlog.com

404 Error

The typical 404 error is shown when a page that doesn’t exist in the server is requested. But, in the Single Page Applications world, the client doesn’t need to make extra requests for every page. Frequently the ‘page’ is already in the client, and what is requested is just the data that will fill the template.

So, when should we show a 404 Error page? Looks like the case is when the app is fetching data for a new ‘page’, that is to say, when a route change is involved and the data is not available. This is the case of:

  • Url changes (there is a broken link or the user types an un-existent address).
  • Resolve guards (a data resolve for a route fails).

To handle the un-existent urls, we can declare an special route with a wildcard (**) that will match any un-existent route:

// errors-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ErrorsComponent } from '../errors-component/errors.component';const routes: Routes = [
{ path: 'error', component: ErrorsComponent },
{ path: '**', component: ErrorsComponent, data: { error: 404 } },
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]

})
export class ErrorRoutingModule { }

This wildcard will load our ErrorsComponent passing the 404 Error to it.

To handle the resolve errors, we can listen to the NavigationError in the router. We can subscribe to it in the same ErrorService:

// errors.service.ts
@Injectable
()
export class ErrorsService {
constructor(
private injector: Injector,
private router: Router,
) {
// Listen to the navigation errors
this.router
.events
.subscribe((event: Event) => {
// Redirect to the ErrorComponent
if (event instanceof NavigationError) {
if (!navigator.onLine) { return; }
// Redirect to the ErrorComponent
this.log(event.error)
.subscribe((errorWithContext) => {
this.router.navigate(['/error'], { queryParams: errorWithContext });
});
}
});

}
...
}

Now, when a resolve fails, it will redirect to the ErrorComponent with the error data as a query param. Click on the “Go to Page” button to see it.

I really missed being able to detect a NavigationError from the ErrorHandler, it would make sense to me, but right now when a data resolve fails Angular throws a “Uncaught (in promise)…” client error.

Ok, that’s it. You saw how 💩 happens. Now, what happens when it happens is On You.

Thanks for reading!

You’ve learned something new, you deserve a gift:

--

--