One of the most repetitive and dull tasks in any application is Error handling. What we want to do is develop a habit, or a pattern, by which we catch errors, and treat them, without much thought of whether we missed something or not. In this post, I will attempt to organize error handling in Angular.
A bug's life
Errors are usually our fault, or somebody else's fault. Today I am concerned with the latter. Those are third party library errors, and API related errors. It begins in the business layer.
Catching it is via an RxJS
operator, or a try ... catch
statement. The business is not responsible for treating the error, thus it should rethrow it, after redressing it.
In the consumer component (UI layer), we can catch the error and treat it. The reaction can be a toast message, a redirect, a scroll to error, a dialog, etc. You can always give it the "silent treatment"😏. If we do not do that, Angular Error Handler in the core of our application, should finally handle it, by logging it, and probably notifying a tracker.
UI vs backend error messages
API services have their own way of returning errors, even if there is usually a global understanding of how they should be built. The errors returned from the backend are non contextual, and not so user friendly, no matter how much pride the database developer holds for them. They are simply not enough. When we talk about toast messages next week, I'll give you an example to prove it.
Fortunately, lately I am seeing it more often that server errors are returning with "code". We can make use of those codes in our UI to recreate those error messages.
First, working backwards, here is an example of a component, making a call, that returns a simple error message (of the API point requested).
create(project: Partial<IProject>) {
// handling errors in a better way
this.projectService.CreateProject(project).subscribe({
next: (data) => {
console.log(data?.id);
},
error: (error) => {
// do something with error, toast, dialog, or sometimes, silence is gold
console.log(error);
}
});
}
// in a simpler non-subscribing observable
getProjects() {
this.projects$ = this.projectService.GetProjects().pipe(
catchError(error => {
// do something with error
console.log(error);
// then continue, nullifying
return of(null);
})
)
}
RxJS custom operator: rethrow
This, as it is, is not powerful enough. The errors caught do not necessarily look as expected. Instead, we will create a* custom operator for the observable*, like we did for the debug operator, only for catchError
. This will prepare the shape of the error as we expect it site-wise:
// custom RxJS operator
export const catchAppError = (message: string): MonoTypeOperatorFunction<any> => {
return pipe(
catchError(error => {
// prepare error here, then rethrow, so that subscriber decides what to do with it
const e = ErrorModelMap(error);
return throwError(() => e);
})
);
};
This operator can be piped in our Http interceptor to catch all response errors:
// in our http interceptor
return next
.handle(adjustedReq)
.pipe(
// debug will take care of logging
debug(`${req.method} ${req.urlWithParams}`, 'p'),
// catch, will prepare the shape of error
catchAppError(`${req.method} ${req.urlWithParams}`)
)
Error model: redress
The error model in UI can contain at least the following:
- Error code: will be translated to UI to get the right UI message
- Error message: coming from server, non contextual, pretty techy and useless to users, but good for developers
- Error status: HTTP response if any, it might come in handy
// in error.model
export interface IUiError {
code: string;
message?: string;
status?: number;
}
We need to return that error in our catchError
operator, we need to map it before we send it along. For that, we need to speak to our typically anti-social API developer, because format is decided by him or her.
Assuming a server error comes back like this (quite common around the web)
{
"error": [
{
"message": "Database failure cyclic gibberish line 34-44 file.py",
"code": "PROJECT_ADD_FAILED"
}
]
}
The UiError
mapper looks like this, brace yourselves for the carnival:
// add this the error.model file
export const UiError = (error: any): IUiError => {
let e: IUiError = {
code: 'Unknown',
message: error,
status: 0,
};
if (error instanceof HttpErrorResponse) {
// map general error
e.message = error.message || '';
e.status = error.status || 0;
// dig out the message if found
if (error.error?.errors?.length) {
// accumulate all errors
const errors = error.error.errors;
e.message = errors.map((l: any) => l.message).join('. ');
// code of first error is enough for ui
e.code = errors[0].code || 'Unknown';
}
}
return e;
};
Our RxJS
operator now can use this mapper:
// custom operator
export const catchAppError = (message: string): MonoTypeOperatorFunction<any> => {
return pipe(
catchError(error => {
// map first
const e = UiError(error);
// then rethrow
return throwError(() => e);
})
);
};
Unfortunately, you will not find two public APIs that return the same error format. Thus, you need to create a manual mapper for every specific type, individually. Will not go into details about this.
In our previous attempt to create a debug custom operator, we logged out the errors as well. But now that we have a new operator, we should remove the logger from the debug operator, and place it in our new operator, to log the error exactly how we expect it down the line.
// update debug operator, remove error handling
export const debug = (message: string, type?: string): MonoTypeOperatorFunction<any> => {
return pipe(
tap({
next: nextValue => {
// ...
},
// remove this part
// error: (error) => {
// ...
// }
})
);
};
// custom operator, add debugging
export const catchAppError = (message: string): MonoTypeOperatorFunction<any> => {
return pipe(
catchError((error) => {
// map out to our model
const e = UiError(error);
// log
_debug(e, message, 'e');
// throw back to allow UI to handle it
return throwError(() => e);
})
);
};
Component treatment
Up till now, all we did is pass through the error as it is from the server. The most popular way to handle those errors is a Toast message. But a toast, is an epic. We'll talk about the toast next week. 😴
Thank you for reading this far, let me know if I burned something.
The project is on going on StackBlitz.
Top comments (0)