Catching and handling errors in Angular

ayyash

Ayyash

Posted on May 18, 2022

Catching and handling errors in Angular

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);
    })
  )
}
Enter fullscreen mode Exit fullscreen mode

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);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

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}`)
  )
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
     }
   ]
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
        })
    );
};
Enter fullscreen mode Exit fullscreen mode

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);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

RESOURCES

RELATED POSTS

💖 💪 🙅 🚩
ayyash
Ayyash

Posted on May 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related