Errors and Angular
Tags: javascript, angular
Published: 2022-07-11 08:54:50

_Original cover photo by [Brett Jordan](https://unsplash.com/@brett_jordan) on Unsplash._

## No one likes Errors

As the title of this paragraph suggests, errors are generally something we try to avoid, and this is especially true for new developers.
But, as a matter of fact, errors are an essential part of the development process, and can even be our friends. So, we need to be careful about how we handle them. Also, it is always better to get a detailed error with a concise description of the problem, and maybe even an explanation of the solution.

Errors also happen in Angular, of course, and today we are going to examine how we can handle errors in Angular, how to differentiate between their types, and also when to throw our own, custom errors. So, let's get started!

## Have a dedicated error handling service

It is always a good idea to have specific logic like logging, error handling and such to Angular services. Now such error handling service
has to be very flexible, and be able to handle different types of errors. For this example, we will put a difference between
HTTP request related errors and other system errors. Here is an example of such an error service:

```ts
import { Injectable } from "@angular/core";
import { HttpErrorResponse } from "@angular/common/http";

enum HttpErrorCodes {
  BadRequest = 400,
  Unauthorized = 401,
  Forbidden = 403,
  ServerError = 500,
}

@Injectable()
export class ErrorService {
  handleError(error: Error) {
    if (error instanceof HttpErrorResponse) {
      this.handleHttpError(error);
    } else {
      // other handling
      console.error("Error:", error);
    }
  }

  private handleHttpError(error: HttpErrorResponse) {
    switch (error.status) {
      case HttpErrorCodes.BadRequest:
        // handle bad request error
        break;
      case HttpErrorCodes.Unauthorized:
        // handle unauthorized error
        break;
      case HttpErrorCodes.Forbidden:
        // handle forbidden error
        break;
      case HttpErrorCodes.ServerError:
        // handle server error
        break;
      default:
        // handle other http error
        break;
    }
  }
}
```

So this is a basic service that handles all sorts of errors, and makes a distinction between HTTP errors and other errors. How can this be improved? For example, if we use some third party error tracking/logging, we can do this in this service by checking our environment:

```ts
export class ErrorService {
  constructor(
    private readonly environment: Environment,
    private readonly logger: LoggerService
  ) {}

  handleError(error: Error) {
    if (error instanceof HttpErrorResponse) {
      this.handleHttpError(error);
    } else {
      // other handling
      console.error("Error:", error);
    }

    // now, let's check the environment, and,
    // when in production, send the error info to our logging tool:
    if (this.environment.production) {
      this.logger.logError(error);
    }
  }

  // other methods
}
```

Now, with this service, we are ready to start our journey into error handling.

## Throwing errors when we know something is wrong

Sometimes we might make HTTP requests which will return an `OK` result, but the response data itself might indicate that not everything went well. For example, if we try to get a user that does not exist, we might get a `404` response, or we might get a `{ "error": "User not found" }` response. Now, if we use `Observables` for HTTP requests, expected operators might become useless when dealing with this:

```ts
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    catchError((error) => {
      // this WILL NOT work!
      // handle the error
      return of(null);
    })
  );

  constructor(private readonly userService: UserService) {}
}
```

Now, we might think we have handled the error, but in reality, if the API returns an `200 OK` response,
but with an explanation why we cannot access this user's data, we will not have a `404` error, and the handler will not be called.
How can this be fixed for HTTP requests? Now, we can, of course, write something like this:

```ts
@Component({
  ...
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    map(response => {
      if (response.success) {
        return response.data;
      } else {
        return throwError(error);
      }
    }),
  );

  constructor(private readonly userService: UserService) {
  }
}
```

But keep in mind that we can have multiple calls with this same error-not-error logic, and we would need to copy-paste this logic everywhere.
Alternatively, we can write a custom RxJS operator to do this, but then we will have to import it everywhere only to do this specific thing.
None of this solutions are ideal, so here is a better one: Let's write an `HttpInterceptor` to check this and throw an error for us:

```ts
export class ResponseErrorInterceptor implements HttpInterceptor {
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler,
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          if (
            event.body?.hasOwnProperty('success') &&
            !event.body.success
          ) {
            throw new HttpErrorResponse({
              status: 400,
              url: event.url,
              error: event.body.error ?? 'Unknown Error',
            });
          }
        }
        return event;
      }),
    );
  }
}
```

Now in this case we handle a `success` property on a response object, but we can perform any kind of logic, and also have multiple interceptors like this to handle different scenarios. Now we can use the `catchError` operator to handle this:

```ts
@Component({
  ...
})
export class AppComponent {
  user$ = this.userService.getUser().pipe(
    catchError(error => of(handleError(error))),
  );

  constructor(private readonly userService: UserService) {
  }
}
```

## Validating data

Sometimes the type system that TypeScript provides is not enough to safeguard some algorithms. This is particularly common with Angular pipes, when we need to provide a specific form of data, otherwise our logic is going to have problems. For instance, if we have a pipe that finds a user by id we need to make sure the data is not only a number, but also, for example, is an integer and a positive number. 
Otherwise, we might get a result that does not reflect reality if we accidentally pass a number that cannot in any way be a number.
To safeguard ourselves and other developers that might use our pipe from this, we can check and throw an error:

```ts
class IdError extends RangeError {
    constructor(providedNumber: number) {
        super(
          `Provided number ${providedNumber} is not a valid id. 
           It must be a positive integer.`);
    }
}

@Pipe({
    name: "findUserById",
})
export class FindUserByIdPipe implements PipeTransform {
    transform(users: User[], id: number): User {
        if (!(Number.isInteger(id) && id > 0)) {
            throw new IdError(id);
        }
        const user = users.find(user => user.id === id);
        return user;
    }
}
```

Now this specific error warns the user of a problem with the provided data, and we won't be dealing with a surprising
or hard-to-catch bug.

> Notice we extend from `RangeError` and not just `Error`. Sometimes it is useful to extend from more specific error types
for better structure. In our case, this specific error *is* in fact a `RangeError`, so we use that class to extend.

## Warning of design nuances/issues

Another class of use cases with errors is when we want to communicate to other developers about some code design specific.
A situation like this can arise, for example, when we have a component that accepts one input or another, but when provided with both
can result in an unexpected behavior. In this case, it is a good practice to throw an error, and let the developer
who uses our component know what is wrong:

```ts
class ComponentInputCompatibilityError extends Error {
    constructor(...properties: string[]) {
        super(`The following properties cannot be provided
               simultaneously: ${properties.join(', ')}`);
    }
}

@Component({
  ...
})
export class MyComponent {
    @Input() property: string;
    @Input() otherProperty: number;

    ngOnChanges(changes: SimpleChanges) {
        if (this.property && this.otherProperty) {
            throw new ComponentInputCompatibilityError(
                'property',
                'otherProperty',
            );
        }
    }
}
```

In this case, we are now safe that no one will confuse and end up with a strange bug.

> **Warning**: This use case might be indicative of a design issue, so most of the time the best practice would be to
> refactor the code to avoid this situation. But of course there are times when changing large chunks of code is not viable.
> In these cases, providing a descriptive error is very important.

## In Conclusion

Errors are a powerful tool to help us to understand the code we are writing, and to make sure we don't have any unexpected behavior.
As we have seen, there are different scenarios in Angular apps when we need to either handle or throw errors, so feel free to use this article as a reference any time you are dealing with risky code.