Angular Error Handling With SOLID Principles

Elias Martinez
The Startup
Published in
9 min readFeb 1, 2021

--

Server logging is so important to detect application problems and there are many solutions already out there that help you out to capture, analyze and some also go beyond even offers you to tell the exact commit and who could be the best candidate (git blame, is that you?) who can work and fix it (Sentry-coff-coff). That’s pretty cool, handy or you put the nice adjective.

Photo by Markus Spiske on Unsplash

However, we all have been in that situation where the company we works for doesn’t want to pay for such solutions because of or whatever reason is. I sort of was that guy who had to look for feasible alternative, in the end, if we do it properly, a combination of your own code with an open-source stack can be a great solution that just hit the point and fulfills the requirements.

The requirements

  • To have a mechanism to capture all errors occurring in the browser.
  • To have a utility to log/send events, errors, or messages of different importance levels (you know it, the classic log levels: info, warn, debug, error, etc).
  • As a system, we required components capable of collect & store “log messages/events”
  • As a team, we should be able to see all the captured “logs” that occurred in the last 3 months so the team can analyze errors in different ways (reactive, preventive, monitoring).
  • As a system, it is required to have alert messages triggered realtime to inform the support team about “critical errors” occurring in the application

The overall Architecture

In a big big picture, the solution can be composed of two main macro-components:

  1. The Collector, Storage, and Dashboard for the messages
  2. The browser utilities to capture and drop/send the logs/messages to the collector
Big picture logging Architecture — Elias Martinez

Concrete components

The Collector Storage & Dashboard

The collector we did was a simple Go server which the only task was to receive a JSON formatted message and drop it to the std.out file (will make more sense in the next paragraph)

The storage and dashboard component we can provide by setting up an EKL stack (more precise Elastic Search, Kibana, LogStash & FileBeat) where FileBeat streams the message logged from the Go server and push to a LogStash pipe to slice each attribute and finally drop an index-able message to ElasticSearch. Finally, by the nature of Kibana, we can set up queries, dashboards, and alerts to be sent when considered “critical” messages (defined completely by you) comes in by the nature of occurrence or amount of occurrences.
In this article we will focus on the frontend part, for the backend part maybe in the future I can provide the details on how to implement it.

The Client-Side Big Picture Parts (or Js components)

Here is what we will need for the recipe:

Whuuut!!… Easy my friend, let’s “de-structure” ;) this thing:

1. ErrorHandler

The task of this guy is to listen to ALL errors occurring in the application; fortunately Angular provides an interface to use; another framework/libraries I guess they have their own ways, but in theory, we all should be able to implement an onerror callback.

Screenshot from Mozilla

2. Logger

The Logger is another service that can receive via dependency injection all the instantiated and configured “transporters” and use them to write on each different log level messages and implementation.

3. Transporters

Transporters are an interface whose only task is to “write” a message in a particular way. It’s a Template pattern where the implementation can vary between different targets from the simple console.log, HTTP, slack, sockets, etc. this is what would make the framework flexible to integrate with other targets.

Ok, Where is my money?

Photo by Melanie Pongratz on Unsplash

Let’s see the details, the meaty part, the bull balls, the under the hood, etc.

The ErrorHandler Details

As the picture stands, the ErrorHandler is sort of a proxy for all errors occurring in the application (either Javascript or HTTP errors) to handle them in something like a funnel/hub. Angular provides OOB with his class:

class ErrorHandler {   handleError(error: any): void   }

And the way to implement it is too easy as just override the provider with yours, like:

class MyErrorHandler implements ErrorHandler {   
handleError(error) {
// do something with the exception
}
}

@NgModule({
providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
}
) class AppModule {}

Now taking the advantage of Dependency injection we can easily inject our custom Logger that will be explained in more detail next. So in the end, our ErrorHandler might look like this:

@Injectable()
export class MyErrorHandler implements ErrorHandler {
constructor(@Inject(MyLoggerToken) private logger: MyLogger) {} handleError(error: Error | HttpErrorResponse): void {
this.logger.error(error);
}
}

Here, what we are doing is basically the injection of a given logger component. The implementation is unknown yet, whatever we inject as the Logger only knows what it can do, to log info, debug, warn or error messages.

Now “The LoggerService” details

The Logger is maybe the more complex component because the way we implement will define how useful or flexible it behaves, but since we are providing an interface/contract it can be later be replaced for something “better” if you manage to improve it. Another important component are the Transporters which we will see the details later so let’s focus only on the Logger mechanics.

The mechanics of the logger is simply as to say “it needs to drop messages of different levels”, so we need to define the interface among other utilities and interfaces

/**
* Logger Injection token
*/
export const MyLoggerToken =
new InjectionToken<MyLogger>('MyLogger');
/**
* Allowed Log input types to be received by a MyLogger
*/
export type LogTypes = string | Error | HttpErrorResponse;
/**
* Logger Interface. Implementations should implement and expose the following methods: info, debug, warn, error
*/
export interface MyLogger {
info(message: string): void;
debug(message: string): void;
warn(message: string): void;
error(error: LogTypes): void;
}

So the job of the logger is to drop log messages somewhere depending if the configured log level is higher than what it’s being tried to be logged. Therefore we need to define Log levels.

export enum LogLevel {
Info = 4,
Debug = 3,
Warn = 2,
Error = 1,
}

Once we defined the log levels we need to somehow tell the logger what log level is permitted to be logged. for that, we can define an injectable object like LoggerConfiguration

/*
* LoggerConfiguration Token
*/
export const LoggerConfigurationToken =
new InjectionToken<LoggerConfiguration>('LoggerConfiguration');
/**
* LoggerConfiguration object interface
*/
export interface LoggerConfiguration {
level: LogLevel;
}

With proper config interfaces in place we can go ahead and provide them so they can be injected to our Logger service class for later provide it in the application itself ready with all his things up, so let’s see the Logger class implementation:

This is a simplified/basic implementation of a logger service, we consume the config files, we have implemented all the methods in the MyLogger interface and for each transporter configured we transfer the control to write it in the way it is designed to transport it.

Now let’s see the last piece to wire in.

The Transporters

First things first, yes I took inspiration from Winston library. Now, Each transporter is responsible to transport/transfer/write the received payload from the logger (which means it is supposed to be written somewhere) to somewhere/someone else. And this “elsewhere” can be literally whatever (console, HTTP log server, slack channel, email, etc…)

That’s it, that simple thing it has to do. merely and simply “write” whatever he gets. The how to write has to be provided as a multi-service provider. This way we can have multiple providers injected into the LoggerService

Now let’s define a base template abstract class to save us time while implementing each Transporter. To do so we can implement some sort of Template pattern and let’s also introduce one small piece we haven’t mentioned before that we can fix with Dependency Injection which is the transport message template (in red)

Having the AbstractTransporter implementing the Transporter interface will be responsible for registering the transport template which in plain code is just an injected function that will produce a payload of any type based on the CapturedPayload interface we implemented in the logger service.

With the AbstractTransporter ready to be implemented we can start implementing each transporter specificity, like for example console logger, HTTP transporter, slack transporter, email transporter, socket transporter, or third-party transporter (sentry or any other service).

So let’s take a look at a couple of them, Console and Http transporters

ConsoleTransporter

This is probably the basic implementation, we don’t even require any template to be consumed or it can be optional.

Please notice that what we receive in the “doWrite” method is the result of calling the write method in the logger service a the “execute” method

And from the next call in the abstract template class

Since the Template is defined while creating the transporters it means that we can have one template for all transporters OR we can have different templates for all transporters, however, all receive the same contract:

/**
* Final captured event, built-in Logger captures this instance
* before queuing the transport macro task execution, if custom
* Logger implemented it's highly recommended to capture this
* instance before the transporters write processing
*/
export interface CapturedPayload {
payload: LogTypes;
level: LogLevel;
timestamp: Date;
}

The HttpTransporter

Finally for this post. Here a simple sample for an HttpTransporter, it extends our AbstractTransporter class and extends few more attributes to be able to work like the httpClient and extra configuration like the endpoint URL or whatever your application needs to perform HTTP calls (cookies management, Bearer token providers what ever it is…) in this case we only need the endpoint where we want to “write” the payload and the base httpClient, Bearer token (security stuff) is provided in another component of the application.

Photo by Rhys Moult on Unsplash

In this implementation, Let’s use a “batch” processor with a buffer of 500ms to collect all payloads and try to minimize the network calls in case we receive tons of errors (this shouldn’t happen but, we all have horror stories). However, it’s up to you how you implement yours. We are not here to judge the implementation but the overall solution, execution is always debatable right?

All right where do I connect all the pieces? So far we have defined and separated the responsibilities and we got a bunch of open-close components.

The last thing to do is… Provide all the pieces

Yeah, we just need to register the logger, transporters & template and we all can guess where. in the AppModule/CoreModule.

Provide the logger

The Logger can be provided in different ways, either you create all logger functionality in a module (recommended) or directly in the app module or core module(whatever is your root module).

Let’s assume we implemented a module, therefore we can add something like this:

Provide the Transporters and template

The rest of the pieces since they can be implemented in varying vast ways and actually needs to fit with every “user” of the module they should be provided in the app module and via factory providers:

That’s all folks.

End Thoughts

Photo by Marek Szturc on Unsplash

We should always think and keep present scalability and maintainability of whatever we do. SOLID principles are proven to grant you both; notice that in this solution we applied some SOLID principles, like SRP and OCP by categorizing and segregating the the parts & tasks in such a way we protect the components from knowing from each other, Each component (Logger, Transporters & Message Template) are replaceable & interchangeable.

The solution is not yet perfect but is good enough to provide scalability. In the end, this is yet one of many ways to solve this problem, hope it helps, and happy coding my friends!

--

--